Image.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <!--
  2. Assistant de création d'image
  3. https://norserium.github.io/vue-advanced-cropper/
  4. -->
  5. <template>
  6. <lazy-LayoutDialog :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
  19. color="grey lighten-1"
  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="loadImage($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 ot_grey ot_white--text" @click="$emit('close')">
  48. {{ $t('cancel') }}
  49. </v-btn>
  50. <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="save">
  51. {{ $t('save') }}
  52. </v-btn>
  53. </template>
  54. </lazy-LayoutDialog>
  55. </template>
  56. <script lang="ts">
  57. import {defineComponent, onUnmounted, Ref, ref, useContext, useFetch, watch} from '@nuxtjs/composition-api'
  58. import { Cropper } from 'vue-advanced-cropper'
  59. import 'vue-advanced-cropper/dist/style.css';
  60. import {AnyJson, ApiResponse} from "~/types/interfaces";
  61. import {useImageProvider} from "~/composables/data/useImageProvider";
  62. import {WatchStopHandle} from "@vue/composition-api";
  63. import {QUERY_TYPE} from "~/types/enums";
  64. import {File} from "~/models/Core/File";
  65. import {repositoryHelper} from "~/services/store/repository";
  66. export default defineComponent({
  67. components: {
  68. Cropper,
  69. },
  70. props: {
  71. existingImageId:{
  72. type: Number,
  73. required: false
  74. },
  75. ownerId:{
  76. type: Number,
  77. required: false
  78. },
  79. field:{
  80. type: String,
  81. required: true
  82. }
  83. },
  84. fetchOnServer: false,
  85. setup (props, {emit}) {
  86. const {$dataProvider, $config, $dataPersister} = useContext()
  87. const {getOne} = useImageProvider($dataProvider, $config)
  88. const fileToSave = new File()
  89. const cropper:Ref<any> = ref(null)
  90. const image: Ref<AnyJson> = ref({
  91. id: null,
  92. src: null,
  93. file: null,
  94. name: null
  95. })
  96. const coordinates:Ref<AnyJson> = ref({})
  97. const defaultSize = ({ imageSize, visibleArea }: any) => {
  98. return {
  99. width: (visibleArea || imageSize).width,
  100. height: (visibleArea || imageSize).height,
  101. };
  102. }
  103. //Si l'id est renseigné, il faut récupérer l'Item File afin d'avoir les informations de config, le nom, etc.
  104. if(props.existingImageId){
  105. useFetch(async () => {
  106. const result: ApiResponse = await $dataProvider.invoke({
  107. type: QUERY_TYPE.DEFAULT,
  108. url: 'api/files',
  109. id: props.existingImageId
  110. })
  111. const config = JSON.parse(result.data.config)
  112. coordinates.value.left = config.x
  113. coordinates.value.top = config.y
  114. coordinates.value.height = config.height
  115. coordinates.value.width = config.width
  116. image.value.name = result.data.name
  117. image.value.id = result.data.id
  118. })
  119. }
  120. //On récupère l'image...
  121. const { fetchState, imageLoaded } = getOne(props.existingImageId)
  122. const unwatch: WatchStopHandle = watch(imageLoaded, (newValue, oldValue) => {
  123. if (newValue === oldValue || typeof newValue === 'undefined') { return }
  124. image.value.src = newValue
  125. })
  126. /**
  127. * Quand l'utilisateur choisit une image sur sa machine
  128. * @param event
  129. */
  130. const loadImage = (event:any) => {
  131. const { files } = event.target
  132. if (files && files[0]) {
  133. reset()
  134. image.value.name = files[0].name
  135. image.value.src = URL.createObjectURL(files[0])
  136. image.value.file = files[0]
  137. }
  138. }
  139. /**
  140. * Losrque le cropper change de position/taille, on met à jour les coordonnées
  141. * @param config
  142. */
  143. const onChange = ({ coordinates: config } : any) => {
  144. coordinates.value = config;
  145. }
  146. /**
  147. * Lorsque l'on sauvegarde l'image
  148. */
  149. const save = async () => {
  150. fileToSave.config = JSON.stringify({
  151. x: coordinates.value.left,
  152. y: coordinates.value.top,
  153. height: coordinates.value.height,
  154. width: coordinates.value.width
  155. })
  156. //Cas d'un PUT : l'image existe déjà on bouge simplement le cropper
  157. if(image.value.id){
  158. fileToSave.id = image.value.id
  159. repositoryHelper.persist(File, fileToSave)
  160. await $dataPersister.invoke({
  161. type: QUERY_TYPE.MODEL,
  162. model: File,
  163. id: fileToSave.id
  164. })
  165. //On émet un évent afin de mettre à jour le formulaire de départ
  166. emit('reload')
  167. }
  168. //Post : on créer une nouvelle image donc on passe par l'api legacy...
  169. else{
  170. if(image.value.file){
  171. //On créer l'objet File à sauvegarder
  172. fileToSave.name = image.value.name
  173. fileToSave.imgFieldName = props.field
  174. fileToSave.visibility = 'EVERYBODY'
  175. fileToSave.folder = 'IMAGES'
  176. if(props.ownerId)
  177. fileToSave.ownerId = props.ownerId
  178. //Appel au datapersister
  179. const response: ApiResponse = await $dataPersister.invoke({
  180. type: QUERY_TYPE.FILE,
  181. baseUrl: $config.baseURL_Legacy,
  182. data: fileToSave.$toJson(),
  183. file: image.value.file
  184. })
  185. //On émet un évent afin de mettre à jour le formulaire de départ
  186. emit('update', response.data['@id'])
  187. }else{
  188. //On reset l'image : on a appuyer sur "poubelle" puis on enregistre
  189. emit('reset')
  190. }
  191. }
  192. }
  193. /**
  194. * On choisit de supprimer l'image présente
  195. */
  196. const reset = () => {
  197. image.value.src = null
  198. image.value.file = null
  199. image.value.name = null
  200. image.value.id = null
  201. URL.revokeObjectURL(image.value.src)
  202. }
  203. /**
  204. * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
  205. */
  206. onUnmounted(() => {
  207. unwatch()
  208. if (image.value.src) {
  209. URL.revokeObjectURL(image.value.src)
  210. }
  211. })
  212. return {
  213. image,
  214. save,
  215. loadImage,
  216. cropper,
  217. reset,
  218. fetchState,
  219. coordinates,
  220. onChange,
  221. defaultSize
  222. }
  223. }
  224. })
  225. </script>
  226. <style lang="scss">
  227. .vue-advanced-cropper__stretcher{
  228. height: auto !important;
  229. width: auto !important;
  230. }
  231. .loading{
  232. height: 300px;
  233. }
  234. .upload {
  235. user-select: none;
  236. padding: 20px;
  237. display: block;
  238. &__cropper {
  239. border: solid 1px #eee;
  240. min-height: 500px;
  241. max-height: 500px;
  242. }
  243. &__cropper-wrapper {
  244. position: relative;
  245. }
  246. &__reset-button {
  247. position: absolute;
  248. right: 20px;
  249. bottom: 20px;
  250. cursor: pointer;
  251. display: flex;
  252. align-items: center;
  253. justify-content: center;
  254. height: 42px;
  255. width: 42px;
  256. background: rgba(#3fb37f, 0.7);
  257. transition: background 0.5s;
  258. &:hover {
  259. background: #3fb37f;
  260. }
  261. }
  262. &__buttons-wrapper {
  263. display: flex;
  264. justify-content: center;
  265. margin-top: 17px;
  266. }
  267. &__button {
  268. border: none;
  269. outline: solid transparent;
  270. color: white;
  271. font-size: 16px;
  272. padding: 10px 20px;
  273. background: #3fb37f;
  274. cursor: pointer;
  275. transition: background 0.5s;
  276. margin: 0 16px;
  277. &:hover,
  278. &:focus {
  279. background: #38d890;
  280. }
  281. input {
  282. display: none;
  283. }
  284. }
  285. }
  286. </style>