| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- <!--
- Assistant de création d'image
- https://norserium.github.io/vue-advanced-cropper/
- -->
- <template>
- <lazy-LayoutDialog :show="true">
- <template #dialogType>{{ $t('image_assistant') }}</template>
- <template #dialogTitle>{{ $t('modif_picture') }}</template>
- <template #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 #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 "~/composables/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>
|