|
|
@@ -0,0 +1,305 @@
|
|
|
+<!--
|
|
|
+Assistant de création d'image
|
|
|
+https://norserium.github.io/vue-advanced-cropper/
|
|
|
+-->
|
|
|
+<template>
|
|
|
+ <lazy-LayoutDialog :show="true">
|
|
|
+ <template v-slot:dialogType>{{ $t('image_assistant') }}</template>
|
|
|
+ <template v-slot:dialogTitle>{{ $t('modif_picture') }}</template>
|
|
|
+ <template v-slot:dialogText>
|
|
|
+ <div class="upload">
|
|
|
+ <v-row
|
|
|
+ v-if="fetchState.pending"
|
|
|
+ class="fill-height ma-0 loading"
|
|
|
+ align="center"
|
|
|
+ justify="center"
|
|
|
+ >
|
|
|
+ <v-progress-circular
|
|
|
+ indeterminate
|
|
|
+ color="grey lighten-1"
|
|
|
+ ></v-progress-circular>
|
|
|
+ </v-row>
|
|
|
+
|
|
|
+ <div v-else >
|
|
|
+ <div class="upload__cropper-wrapper">
|
|
|
+ <cropper
|
|
|
+ ref="cropper"
|
|
|
+ class="upload__cropper"
|
|
|
+ check-orientation
|
|
|
+ :src="image.src"
|
|
|
+ :default-position="{left : coordinates.left, top : coordinates.top}"
|
|
|
+ :default-size="coordinates.width ? {width : coordinates.width, height : coordinates.height}: defaultSize"
|
|
|
+ @change="onChange"
|
|
|
+ />
|
|
|
+ <div v-if="image.src" class="upload__reset-button" title="Reset Image" @click="reset()">
|
|
|
+ <v-icon>mdi-delete</v-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="upload__buttons-wrapper">
|
|
|
+ <button class="upload__button" @click="$refs.file.click()">
|
|
|
+ <input ref="file" type="file" accept="image/*" @change="loadImage($event)" />
|
|
|
+ {{$t('upload_image')}}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:dialogBtn>
|
|
|
+ <v-btn class="mr-4 submitBtn ot_grey ot_white--text" @click="$emit('close')">
|
|
|
+ {{ $t('cancel') }}
|
|
|
+ </v-btn>
|
|
|
+ <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="save">
|
|
|
+ {{ $t('save') }}
|
|
|
+ </v-btn>
|
|
|
+ </template>
|
|
|
+ </lazy-LayoutDialog>
|
|
|
+
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import {defineComponent, onUnmounted, Ref, ref, useContext, useFetch, watch} from '@nuxtjs/composition-api'
|
|
|
+import { Cropper } from 'vue-advanced-cropper'
|
|
|
+import 'vue-advanced-cropper/dist/style.css';
|
|
|
+import {AnyJson, ApiResponse} from "~/types/interfaces";
|
|
|
+import {UseImage} from "~/use/data/useImage";
|
|
|
+import {WatchStopHandle} from "@vue/composition-api";
|
|
|
+import {QUERY_TYPE} from "~/types/enums";
|
|
|
+import {File} from "~/models/Core/File";
|
|
|
+import {repositoryHelper} from "~/services/store/repository";
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ components: {
|
|
|
+ Cropper,
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ existingImageId:{
|
|
|
+ type: Number,
|
|
|
+ required: false
|
|
|
+ },
|
|
|
+ ownerId:{
|
|
|
+ type: Number,
|
|
|
+ required: false
|
|
|
+ },
|
|
|
+ field:{
|
|
|
+ type: String,
|
|
|
+ required: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fetchOnServer: false,
|
|
|
+ setup (props, {emit}) {
|
|
|
+ const fileToSave = new File()
|
|
|
+ const cropper:Ref<any> = ref(null)
|
|
|
+ const image: Ref<AnyJson> = ref({
|
|
|
+ id: null,
|
|
|
+ src: null,
|
|
|
+ file: null,
|
|
|
+ name: null
|
|
|
+ })
|
|
|
+ const coordinates:Ref<AnyJson> = ref({})
|
|
|
+ const defaultSize = ({ imageSize, visibleArea }: any) => {
|
|
|
+ return {
|
|
|
+ width: (visibleArea || imageSize).width,
|
|
|
+ height: (visibleArea || imageSize).height,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const {$dataProvider, $config, $dataPersister} = useContext()
|
|
|
+
|
|
|
+ //Si l'id est renseigné, il faut récupérer l'Item File afin d'avoir les informations de config, le nom, etc.
|
|
|
+ if(props.existingImageId){
|
|
|
+ useFetch(async () => {
|
|
|
+ const result: ApiResponse = await $dataProvider.invoke({
|
|
|
+ type: QUERY_TYPE.DEFAULT,
|
|
|
+ url: 'api/files',
|
|
|
+ id: props.existingImageId
|
|
|
+ })
|
|
|
+
|
|
|
+ const config = JSON.parse(result.data.config)
|
|
|
+ coordinates.value.left = config.x
|
|
|
+ coordinates.value.top = config.y
|
|
|
+ coordinates.value.height = config.height
|
|
|
+ coordinates.value.width = config.width
|
|
|
+ image.value.name = result.data.name
|
|
|
+ image.value.id = result.data.id
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ //On récupère l'image...
|
|
|
+ const { fetchState, imageLoaded } = new UseImage().getOne(props.existingImageId)
|
|
|
+ const unwatch: WatchStopHandle = watch(imageLoaded, (newValue, oldValue) => {
|
|
|
+ if (newValue === oldValue || typeof newValue === 'undefined') { return }
|
|
|
+ image.value.src = newValue
|
|
|
+ })
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Quand l'utilisateur choisit une image sur sa machine
|
|
|
+ * @param event
|
|
|
+ */
|
|
|
+ const loadImage = (event:any) => {
|
|
|
+ const { files } = event.target
|
|
|
+ if (files && files[0]) {
|
|
|
+ reset()
|
|
|
+ image.value.name = files[0].name
|
|
|
+ image.value.src = URL.createObjectURL(files[0])
|
|
|
+ image.value.file = files[0]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Losrque le cropper change de position/taille, on met à jour les coordonnées
|
|
|
+ * @param config
|
|
|
+ */
|
|
|
+ const onChange = ({ coordinates: config } : any) => {
|
|
|
+ coordinates.value = config;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Lorsque l'on sauvegarde l'image
|
|
|
+ */
|
|
|
+ const save = async () => {
|
|
|
+ fileToSave.config = JSON.stringify({
|
|
|
+ x: coordinates.value.left,
|
|
|
+ y: coordinates.value.top,
|
|
|
+ height: coordinates.value.height,
|
|
|
+ width: coordinates.value.width
|
|
|
+ })
|
|
|
+
|
|
|
+ //Cas d'un PUT : l'image existe déjà on bouge simplement le cropper
|
|
|
+ if(image.value.id){
|
|
|
+ fileToSave.id = image.value.id
|
|
|
+ repositoryHelper.persist(File, fileToSave)
|
|
|
+ await $dataPersister.invoke({
|
|
|
+ type: QUERY_TYPE.MODEL,
|
|
|
+ model: File,
|
|
|
+ id: fileToSave.id
|
|
|
+ })
|
|
|
+ //On émet un évent afin de mettre à jour le formulaire de départ
|
|
|
+ emit('reload')
|
|
|
+ }
|
|
|
+
|
|
|
+ //Post : on créer une nouvelle image donc on passe par l'api legacy...
|
|
|
+ else{
|
|
|
+ if(image.value.file){
|
|
|
+ //On créer l'objet File à sauvegarder
|
|
|
+ fileToSave.name = image.value.name
|
|
|
+ fileToSave.imgFieldName = props.field
|
|
|
+ fileToSave.visibility = 'EVERYBODY'
|
|
|
+ fileToSave.folder = 'IMAGES'
|
|
|
+
|
|
|
+ if(props.ownerId)
|
|
|
+ fileToSave.ownerId = props.ownerId
|
|
|
+
|
|
|
+ //Appel au datapersister
|
|
|
+ const response: ApiResponse = await $dataPersister.invoke({
|
|
|
+ type: QUERY_TYPE.FILE,
|
|
|
+ baseUrl: $config.baseURL_Legacy,
|
|
|
+ data: fileToSave.$toJson(),
|
|
|
+ file: image.value.file
|
|
|
+ })
|
|
|
+ //On émet un évent afin de mettre à jour le formulaire de départ
|
|
|
+ emit('update', response.data['@id'])
|
|
|
+ }else{
|
|
|
+ //On reset l'image : on a appuyer sur "poubelle" puis on enregistre
|
|
|
+ emit('reset')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * On choisit de supprimer l'image présente
|
|
|
+ */
|
|
|
+ const reset = () => {
|
|
|
+ image.value.src = null
|
|
|
+ image.value.file = null
|
|
|
+ image.value.name = null
|
|
|
+ image.value.id = null
|
|
|
+ URL.revokeObjectURL(image.value.src)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
|
|
|
+ */
|
|
|
+ onUnmounted(() => {
|
|
|
+ unwatch()
|
|
|
+ if (image.value.src) {
|
|
|
+ URL.revokeObjectURL(image.value.src)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ image,
|
|
|
+ save,
|
|
|
+ loadImage,
|
|
|
+ cropper,
|
|
|
+ reset,
|
|
|
+ fetchState,
|
|
|
+ coordinates,
|
|
|
+ onChange,
|
|
|
+ defaultSize
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+ .vue-advanced-cropper__stretcher{
|
|
|
+ height: auto !important;
|
|
|
+ width: auto !important;
|
|
|
+ }
|
|
|
+ .loading{
|
|
|
+ height: 300px;
|
|
|
+ }
|
|
|
+ .upload {
|
|
|
+ user-select: none;
|
|
|
+ padding: 20px;
|
|
|
+ display: block;
|
|
|
+ &__cropper {
|
|
|
+ border: solid 1px #eee;
|
|
|
+ min-height: 500px;
|
|
|
+ max-height: 500px;
|
|
|
+ }
|
|
|
+ &__cropper-wrapper {
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ &__reset-button {
|
|
|
+ position: absolute;
|
|
|
+ right: 20px;
|
|
|
+ bottom: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 42px;
|
|
|
+ width: 42px;
|
|
|
+ background: rgba(#3fb37f, 0.7);
|
|
|
+ transition: background 0.5s;
|
|
|
+ &:hover {
|
|
|
+ background: #3fb37f;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &__buttons-wrapper {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 17px;
|
|
|
+ }
|
|
|
+ &__button {
|
|
|
+ border: none;
|
|
|
+ outline: solid transparent;
|
|
|
+ color: white;
|
|
|
+ font-size: 16px;
|
|
|
+ padding: 10px 20px;
|
|
|
+ background: #3fb37f;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.5s;
|
|
|
+ margin: 0 16px;
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ background: #38d890;
|
|
|
+ }
|
|
|
+ input {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|