Modulo Administración #78
|
@ -75,6 +75,10 @@ const props = defineProps({
|
||||||
saveFn: {
|
saveFn: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
separationBetweenInputs: {
|
||||||
|
type: String,
|
||||||
|
default: 'xs'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,6 +109,10 @@ const updatedColumns = computed(() => {
|
||||||
|
|
||||||
const hasChanges = computed(() => !!updatedColumns.value.length);
|
const hasChanges = computed(() => !!updatedColumns.value.length);
|
||||||
|
|
||||||
|
const separationBetweenInputs = computed(() => {
|
||||||
|
return `q-gutter-y-${props.separationBetweenInputs}`;
|
||||||
|
});
|
||||||
|
|
||||||
const fetchFormData = async () => {
|
const fetchFormData = async () => {
|
||||||
if (!props.fetchFormDataSql.query) return;
|
if (!props.fetchFormDataSql.query) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
@ -173,6 +181,8 @@ const generateSqlQuery = () => {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!props.formInitialData && props.autoLoad) {
|
if (!props.formInitialData && props.autoLoad) {
|
||||||
fetchFormData();
|
fetchFormData();
|
||||||
|
} else {
|
||||||
|
formData.value = { ...props.formInitialData };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -187,10 +197,11 @@ defineExpose({
|
||||||
<QForm
|
<QForm
|
||||||
v-if="!loading"
|
v-if="!loading"
|
||||||
ref="addressFormRef"
|
ref="addressFormRef"
|
||||||
class="column full-width q-gutter-y-xs"
|
class="column full-width"
|
||||||
|
:class="separationBetweenInputs"
|
||||||
@submit="submit()"
|
@submit="submit()"
|
||||||
>
|
>
|
||||||
<span class="text-h6 text-bold">
|
<span v-if="title" class="text-h6 text-bold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<slot name="form" :data="formData" />
|
<slot name="form" :data="formData" />
|
||||||
|
@ -220,7 +231,7 @@ defineExpose({
|
||||||
flat
|
flat
|
||||||
:disabled="!showBottomActions && !updatedColumns.length"
|
:disabled="!showBottomActions && !updatedColumns.length"
|
||||||
/>
|
/>
|
||||||
<slot name="actions" />
|
<slot name="actions" :data="formData" />
|
||||||
</component>
|
</component>
|
||||||
</QForm>
|
</QForm>
|
||||||
<QSpinner v-else color="primary" size="3em" :thickness="2" />
|
<QSpinner v-else color="primary" size="3em" :thickness="2" />
|
||||||
|
|
|
@ -102,20 +102,25 @@ en-US:
|
||||||
name: Name
|
name: Name
|
||||||
file: File
|
file: File
|
||||||
send: Send
|
send: Send
|
||||||
|
imageAdded: Image added successfully
|
||||||
es-ES:
|
es-ES:
|
||||||
name: Nombre
|
name: Nombre
|
||||||
file: Archivo
|
file: Archivo
|
||||||
send: Enviar
|
send: Enviar
|
||||||
|
imageAdded: Imagen añadida correctamente
|
||||||
ca-ES:
|
ca-ES:
|
||||||
name: Nom
|
name: Nom
|
||||||
file: Arxiu
|
file: Arxiu
|
||||||
send: Enviar
|
send: Enviar
|
||||||
|
imageAdded: Imatge afegida correctament
|
||||||
fr-FR:
|
fr-FR:
|
||||||
name: Nom
|
name: Nom
|
||||||
file: Fichier
|
file: Fichier
|
||||||
send: Envoyer
|
send: Envoyer
|
||||||
|
imageAdded: Image ajoutée correctement
|
||||||
pt-PT:
|
pt-PT:
|
||||||
name: Nome
|
name: Nome
|
||||||
file: Arquivo
|
file: Arquivo
|
||||||
send: Enviar
|
send: Enviar
|
||||||
|
imageAdded: Imagen adicionada corretamente
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
|
@ -59,6 +59,5 @@ export default {
|
||||||
adminPhotos: 'Imatges',
|
adminPhotos: 'Imatges',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
|
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
|
||||||
at: 'a les',
|
at: 'a les'
|
||||||
imageAdded: 'Imatge afegida correctament'
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -73,7 +73,6 @@ export default {
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Order loaded into basket!',
|
orderLoadedIntoBasket: 'Order loaded into basket!',
|
||||||
at: 'at',
|
at: 'at',
|
||||||
imageAdded: 'Image added successfully',
|
|
||||||
|
|
||||||
orders: 'Orders',
|
orders: 'Orders',
|
||||||
order: 'Pending order',
|
order: 'Pending order',
|
||||||
|
|
|
@ -59,6 +59,5 @@ export default {
|
||||||
adminPhotos: 'Images',
|
adminPhotos: 'Images',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
|
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
|
||||||
at: 'à',
|
at: 'à'
|
||||||
imageAdded: 'Image ajoutée correctement'
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,5 @@ export default {
|
||||||
adminPhotos: 'Imagens',
|
adminPhotos: 'Imagens',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
|
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
|
||||||
at: 'às',
|
at: 'às'
|
||||||
imageAdded: 'Imagen adicionada corretamente'
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref, onMounted, inject, reactive, computed } from 'vue';
|
||||||
|
|
||||||
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const api = inject('api');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
|
const fileUploaderRef = ref(null);
|
||||||
|
const statusIcons = {
|
||||||
|
uploading: 'cloud_upload',
|
||||||
|
fulfilled: 'cloud_done',
|
||||||
|
rejected: 'error',
|
||||||
|
pending: 'add'
|
||||||
|
};
|
||||||
|
const formInitialData = reactive({
|
||||||
|
schema: 'catalog',
|
||||||
|
updateMatching: true
|
||||||
|
});
|
||||||
|
const imageCollections = ref([]);
|
||||||
|
const addedFiles = ref([]);
|
||||||
|
|
||||||
|
const isSubmitable = computed(() =>
|
||||||
|
addedFiles.value.some(file => file.uploadStatus === 'pending')
|
||||||
|
);
|
||||||
|
|
||||||
|
const getImageCollections = async () => {
|
||||||
|
try {
|
||||||
|
imageCollections.value = await jApi.query(
|
||||||
|
'SELECT name, `desc` FROM imageCollection ORDER BY `desc`'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting image collections:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async data => {
|
||||||
|
if (!addedFiles.value.length) {
|
||||||
|
notify(t('noFilesToUpload'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filteredFiles = addedFiles.value.filter(
|
||||||
|
file => file.uploadStatus === 'pending'
|
||||||
|
);
|
||||||
|
const promises = filteredFiles.map((file, index) => {
|
||||||
|
const fileIndex = filteredFiles[index].index;
|
||||||
|
addedFiles.value[fileIndex].uploadStatus = 'uploading';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('updateMatching', data.updateMatching);
|
||||||
|
formData.append('image', file.file);
|
||||||
|
formData.append('name', file.name);
|
||||||
|
formData.append('schema', data.schema);
|
||||||
|
formData.append('srv', 'json:image/upload');
|
||||||
|
return api({
|
||||||
|
method: 'post',
|
||||||
|
url: location.origin,
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const fileIndex = filteredFiles[index].index;
|
||||||
|
addedFiles.value[fileIndex].uploadStatus = result.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
const allSuccessful = results.every(
|
||||||
|
result => result.status === 'fulfilled'
|
||||||
|
);
|
||||||
|
if (allSuccessful) {
|
||||||
|
notify(t('uploadSuccess'), 'positive');
|
||||||
|
} else {
|
||||||
|
notify(t('uploadError'), 'negative');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilesAdded = files => {
|
||||||
|
const initialFilesLength = addedFiles.value.length;
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
const [name] = file.name.split('.');
|
||||||
|
const fileData = {
|
||||||
|
name,
|
||||||
|
file,
|
||||||
|
index: initialFilesLength + index,
|
||||||
|
uploadStatus: 'pending'
|
||||||
|
};
|
||||||
|
addedFiles.value.push(fileData);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const recalculateFilesIndexes = () => {
|
||||||
|
addedFiles.value.forEach((_, index) => {
|
||||||
|
addedFiles.value[index].index = index;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (file, index) => {
|
||||||
|
fileUploaderRef.value.removeFile(file);
|
||||||
|
addedFiles.value.splice(index, 1);
|
||||||
|
recalculateFilesIndexes();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFiles = () => {
|
||||||
|
fileUploaderRef.value.reset();
|
||||||
|
addedFiles.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => getImageCollections());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QPage class="vn-w-sm">
|
||||||
|
<VnForm
|
||||||
|
ref="vnFormRef"
|
||||||
|
:defaultActions="false"
|
||||||
|
:formInitialData="formInitialData"
|
||||||
|
separationBetweenInputs="md"
|
||||||
|
showBottomActions
|
||||||
|
>
|
||||||
|
<template #form="{ data }">
|
||||||
|
<VnSelect
|
||||||
|
v-model="data.schema"
|
||||||
|
:label="t('collection')"
|
||||||
|
option-label="desc"
|
||||||
|
option-value="name"
|
||||||
|
:options="imageCollections"
|
||||||
|
/>
|
||||||
|
<QUploader
|
||||||
|
ref="fileUploaderRef"
|
||||||
|
:label="t('dropYourFiles')"
|
||||||
|
class="full-width"
|
||||||
|
square
|
||||||
|
flat
|
||||||
|
multiple
|
||||||
|
bordered
|
||||||
|
hide-upload-btn
|
||||||
|
@added="onFilesAdded"
|
||||||
|
>
|
||||||
|
<template v-slot:list="scope">
|
||||||
|
<QList v-if="addedFiles.length" separator>
|
||||||
|
<QItem
|
||||||
|
v-for="(file, index) in scope.files"
|
||||||
|
:key="file.__key"
|
||||||
|
class="flex full-width row items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="file.__img.src"
|
||||||
|
style="width: 28px; height: 21px"
|
||||||
|
class="q-mr-md"
|
||||||
|
/>
|
||||||
|
<VnInput
|
||||||
|
v-model="addedFiles[index].name"
|
||||||
|
:clearable="false"
|
||||||
|
dense
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
<QSpinner
|
||||||
|
v-if="
|
||||||
|
addedFiles[index].uploadStatus ===
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
color="primary"
|
||||||
|
size="2em"
|
||||||
|
:thickness="1"
|
||||||
|
/>
|
||||||
|
<QIcon
|
||||||
|
v-else-if="
|
||||||
|
addedFiles[index].uploadStatus &&
|
||||||
|
addedFiles[index].uploadStatus !==
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
:name="
|
||||||
|
statusIcons[
|
||||||
|
addedFiles[index].uploadStatus
|
||||||
|
]
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
v-if="
|
||||||
|
addedFiles[index].uploadStatus !==
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
class="gt-xs"
|
||||||
|
size="md"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
icon="delete"
|
||||||
|
@click="removeFile(file, index)"
|
||||||
|
/>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</QUploader>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="data.updateMatching"
|
||||||
|
:label="t('updateMatching')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ data }">
|
||||||
|
<QBtn
|
||||||
|
:label="t('clearAll')"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
@click="clearFiles()"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
:label="t('uploadFiles')"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
:disable="!isSubmitable"
|
||||||
|
@click="onSubmit(data)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VnForm>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
collection: Collection
|
||||||
|
updateMatching: Update items with matching id
|
||||||
|
dropYourFiles: Click or drop files here
|
||||||
|
clearAll: Clear all
|
||||||
|
uploadFiles: Upload files
|
||||||
|
uploadSuccess: Upload finished successfully
|
||||||
|
uploadError: Some errors happened on upload
|
||||||
|
noFilesToUpload: There are no files to upload
|
||||||
|
es-ES:
|
||||||
|
collection: Colección
|
||||||
|
updateMatching: Actualizar artículos con id coincidente
|
||||||
|
dropYourFiles: Pulsa o suelta los archivos aquí
|
||||||
|
clearAll: Limpiar todo
|
||||||
|
uploadFiles: Subir archivos
|
||||||
|
uploadSuccess: Imágenes subidas correctamente
|
||||||
|
uploadError: Ocurrieron errores al subir alguna de las imágenes
|
||||||
|
noFilesToUpload: No se han seleccionado archivos para subir
|
||||||
|
ca-ES:
|
||||||
|
collection: Col·lecció
|
||||||
|
updateMatching: Actualitzar els elements amb id coincident
|
||||||
|
dropYourFiles: Prem o deixa anar els arxius aquí
|
||||||
|
clearAll: Netejar tot
|
||||||
|
uploadFiles: Pujar arxius
|
||||||
|
uploadSuccess: Imatges pujades correctament
|
||||||
|
uploadError: Van ocórrer errors en pujar alguna de les imatges
|
||||||
|
noFilesToUpload: No s'ha seleccionat arxius per pujar
|
||||||
|
fr-FR:
|
||||||
|
collection: Collection
|
||||||
|
updateMatching: Mettre à jour les éléments avec l'identifiant correspondant
|
||||||
|
dropYourFiles: Cliquez ici ou déposer des fichiers
|
||||||
|
clearAll: Tout effacer
|
||||||
|
uploadFiles: Upload Files
|
||||||
|
uploadSuccess: Les images téléchargées correctement
|
||||||
|
uploadError: Des erreurs sont survenues lors du téléchargement des images
|
||||||
|
noFilesToUpload: Aucun fichier sélectionné pour télécharger
|
||||||
|
pt-PT:
|
||||||
|
collection: Coleção
|
||||||
|
updateMatching: Atualizar itens com id correspondente
|
||||||
|
dropYourFiles: Clique ou solte arquivos aqui
|
||||||
|
clearAll: Limpar tudo
|
||||||
|
uploadFiles: Fazer upload de arquivos
|
||||||
|
uploadSuccess: Upload concluído com sucesso
|
||||||
|
uploadError: Ocorreram erros ao subir alguma das imagens
|
||||||
|
noFilesToUpload: Não há arquivos selecionados para upload
|
||||||
|
</i18n>
|
Loading…
Reference in New Issue