Image.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <!--
  2. Assistant de création d'image
  3. https://norserium.github.io/vue-advanced-cropper/
  4. -->
  5. <template>
  6. <LazyLayoutDialog :show="true">
  7. <template #dialogType>{{ $t('image_assistant') }}</template>
  8. <template #dialogTitle>{{ $t('modif_picture') }}</template>
  9. <template #dialogText>
  10. <div class="upload">
  11. <v-row
  12. v-if="fetchState.pending"
  13. class="fill-height ma-0 loading"
  14. align="center"
  15. justify="center"
  16. >
  17. <v-progress-circular
  18. :indeterminate="true"
  19. color="neutral">
  20. </v-progress-circular>
  21. </v-row>
  22. <div v-else >
  23. <div class="upload__cropper-wrapper">
  24. <cropper
  25. ref="cropper"
  26. class="upload__cropper"
  27. check-orientation
  28. :src="image.src"
  29. :default-position="{left : coordinates.left, top : coordinates.top}"
  30. :default-size="coordinates.width ? {width : coordinates.width, height : coordinates.height}: defaultSize"
  31. @change="onChange"
  32. />
  33. <div v-if="image.src" class="upload__reset-button" title="Reset Image" @click="reset()">
  34. <v-icon>mdi-delete</v-icon>
  35. </div>
  36. </div>
  37. <div class="upload__buttons-wrapper">
  38. <button class="upload__button" @click="$refs.file.click()">
  39. <input ref="file" type="file" accept="image/*" @change="uploadImage($event)" />
  40. {{$t('upload_image')}}
  41. </button>
  42. </div>
  43. </div>
  44. </div>
  45. </template>
  46. <template #dialogBtn>
  47. <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="$emit('close')">
  48. {{ $t('cancel') }}
  49. </v-btn>
  50. <v-btn class="mr-4 submitBtn theme-danger" @click="save">
  51. {{ $t('save') }}
  52. </v-btn>
  53. </template>
  54. </LazyLayoutDialog>
  55. </template>
  56. <script setup lang="ts">
  57. import {useNuxtApp} from "#app";
  58. import {ref} from "@vue/reactivity";
  59. import type {Ref} from "@vue/reactivity";
  60. import File from '~/models/Core/File'
  61. import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
  62. import UrlUtils from "~/services/utils/urlUtils";
  63. import {useImageFetch} from "~/composables/data/useImageFetch";
  64. import {onUnmounted, watch} from "@vue/runtime-core";
  65. import type {WatchStopHandle} from "@vue/runtime-core";
  66. import {useEntityManager} from "~/composables/data/useEntityManager";
  67. import type {AnyJson} from "~/types/data";
  68. const props = defineProps({
  69. imageId: {
  70. type: Number,
  71. required: false
  72. },
  73. ownerId: {
  74. type: Number,
  75. required: false
  76. },
  77. field: {
  78. type: String,
  79. required: true
  80. }
  81. })
  82. const { emit } = useNuxtApp()
  83. const { em } = useEntityManager()
  84. const file = new File()
  85. const cropper: Ref<any> = ref(null)
  86. const image: Ref<AnyJson> = ref({
  87. id: null,
  88. src: null,
  89. file: null,
  90. name: null
  91. })
  92. const coordinates: Ref<AnyJson> = ref({})
  93. const defaultSize = ({ imageSize, visibleArea }: any) => {
  94. return {
  95. width: (visibleArea || imageSize).width,
  96. height: (visibleArea || imageSize).height,
  97. };
  98. }
  99. // Si l'id est renseigné, on récupère l'Item File afin d'avoir les informations de config, le nom, etc.
  100. if (props.imageId && props.imageId > 0) {
  101. const { apiRequestService } = useAp2iRequestService()
  102. const result: any = await apiRequestService.get(UrlUtils.join('api/files', '' + props.imageId))
  103. const config = JSON.parse(result.data.config)
  104. coordinates.value.left = config.x
  105. coordinates.value.top = config.y
  106. coordinates.value.height = config.height
  107. coordinates.value.width = config.width
  108. image.value.name = result.data.name
  109. image.value.id = result.data.id
  110. }
  111. //On récupère l'image...
  112. const { fetch } = useImageFetch()
  113. const { data: imageLoaded, pending } = fetch(props.imageId ?? null)
  114. const unwatch: WatchStopHandle = watch(
  115. imageLoaded,
  116. (newValue, oldValue) => {
  117. if (newValue === oldValue || typeof newValue === 'undefined') {
  118. return
  119. }
  120. image.value.src = newValue
  121. }
  122. )
  123. /**
  124. * Quand l'utilisateur choisit une image sur sa machine
  125. * @param event
  126. */
  127. const uploadImage = (event:any) => {
  128. const { files } = event.target
  129. if (files && files[0]) {
  130. reset()
  131. image.value.name = files[0].name
  132. image.value.src = URL.createObjectURL(files[0])
  133. image.value.file = files[0]
  134. }
  135. }
  136. /**
  137. * Lorsque le cropper change de position / taille, on met à jour les coordonnées
  138. * @param config
  139. */
  140. const onChange = ({ coordinates: config } : any) => {
  141. coordinates.value = config;
  142. }
  143. /**
  144. * Lorsque l'on sauvegarde l'image
  145. */
  146. // TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
  147. const save = async () => {
  148. file.config = JSON.stringify({
  149. x: coordinates.value.left,
  150. y: coordinates.value.top,
  151. height: coordinates.value.height,
  152. width: coordinates.value.width
  153. })
  154. if (image.value.id > 0) {
  155. // Mise à jour d'une image existante : on bouge simplement le cropper
  156. file.id = image.value.id as number
  157. await em.persist(File, file) // TODO: à revoir
  158. // On émet un évent afin de mettre à jour le formulaire de départ
  159. emit('reload')
  160. } else {
  161. // Création d'une nouvelle image
  162. if (image.value.file) {
  163. // On créé l'objet File à sauvegarder
  164. file.name = image.value.name
  165. file.imgFieldName = props.field
  166. file.visibility = 'EVERYBODY'
  167. file.folder = 'IMAGES'
  168. file.status = 'READY'
  169. if (props.ownerId) {
  170. file.ownerId = props.ownerId
  171. }
  172. const returnedFile = await em.persist(File, file) // TODO: à revoir, il faudra pouvoir passer `image.value.file` avec la requête
  173. //On émet un évent afin de mettre à jour le formulaire de départ
  174. emit('update', returnedFile.data['@id'])
  175. } else {
  176. // On reset l'image : on a appuyé sur "poubelle" puis on enregistre
  177. emit('reset')
  178. }
  179. }
  180. }
  181. /**
  182. * On choisit de supprimer l'image présente
  183. */
  184. const reset = () => {
  185. image.value.src = null
  186. image.value.file = null
  187. image.value.name = null
  188. image.value.id = null
  189. URL.revokeObjectURL(image.value.src)
  190. }
  191. /**
  192. * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
  193. */
  194. onUnmounted(() => {
  195. unwatch()
  196. if (image.value.src) {
  197. URL.revokeObjectURL(image.value.src)
  198. }
  199. })
  200. </script>
  201. <style lang="scss">
  202. .vue-advanced-cropper__stretcher{
  203. height: auto !important;
  204. width: auto !important;
  205. }
  206. .loading{
  207. height: 300px;
  208. }
  209. .upload {
  210. user-select: none;
  211. padding: 20px;
  212. display: block;
  213. &__cropper {
  214. border: solid 1px rgb(var(--v-theme-on-neutral-strong));;
  215. min-height: 500px;
  216. max-height: 500px;
  217. }
  218. &__cropper-wrapper {
  219. position: relative;
  220. }
  221. &__reset-button {
  222. position: absolute;
  223. right: 20px;
  224. bottom: 20px;
  225. cursor: pointer;
  226. display: flex;
  227. align-items: center;
  228. justify-content: center;
  229. height: 42px;
  230. width: 42px;
  231. background: rgb(var(--v-theme-neutral));
  232. transition: background 0.5s;
  233. &:hover {
  234. background: rgb(var(--v-theme-primary-alt));
  235. }
  236. }
  237. &__buttons-wrapper {
  238. display: flex;
  239. justify-content: center;
  240. margin-top: 17px;
  241. }
  242. &__button {
  243. border: none;
  244. outline: solid transparent;
  245. color: rgb(var(--v-theme-on-neutral));
  246. font-size: 16px;
  247. padding: 10px 20px;
  248. background: rgb(var(--v-theme-neutral));
  249. cursor: pointer;
  250. transition: background 0.5s;
  251. margin: 0 16px;
  252. &:hover,
  253. &:focus {
  254. background: rgb(var(--v-theme-primary-alt));
  255. }
  256. input {
  257. display: none;
  258. }
  259. }
  260. }
  261. </style>