PersonalSettingAvatarCropImageFlyout.vue 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { ref } from 'vue'
  4. import { Cropper, type CropperResult } from 'vue-advanced-cropper'
  5. import 'vue-advanced-cropper/dist/style.css'
  6. import CommonAvatar from '#shared/components/CommonAvatar/CommonAvatar.vue'
  7. import type { ImageFileData } from '#shared/utils/files.ts'
  8. import CommonFlyout from '#desktop/components/CommonFlyout/CommonFlyout.vue'
  9. interface Props {
  10. image?: ImageFileData
  11. }
  12. const props = defineProps<Props>()
  13. defineEmits<{
  14. 'image-cropped': [ImageFileData | undefined]
  15. }>()
  16. const croppedImage = ref<ImageFileData>()
  17. const imageCropped = (crop: CropperResult) => {
  18. if (!crop.canvas) return
  19. croppedImage.value = {
  20. content: crop.canvas.toDataURL('image/png'),
  21. name: 'avatar.png',
  22. type: 'image/png',
  23. }
  24. }
  25. const discardImage = () => {
  26. croppedImage.value = undefined
  27. }
  28. </script>
  29. <template>
  30. <CommonFlyout
  31. :header-title="__('Crop Image')"
  32. :footer-action-options="{
  33. actionLabel: __('Save'),
  34. actionButton: { variant: 'submit' },
  35. }"
  36. header-icon="image"
  37. name="avatar-file-upload"
  38. @action="$emit('image-cropped', croppedImage)"
  39. @close="discardImage"
  40. >
  41. <div class="flex flex-col gap-3">
  42. <div v-if="croppedImage" class="flex flex-row items-center gap-1">
  43. <CommonAvatar :image="croppedImage.content" size="normal" />
  44. <CommonLabel>{{ $t('Avatar Preview') }}</CommonLabel>
  45. </div>
  46. <Cropper
  47. :src="props.image?.content"
  48. :stencil-props="{
  49. aspectRatio: 1,
  50. class: 'cropper-stencil',
  51. previewClass: 'cropper-stencil__preview',
  52. draggingClass: 'cropper-stencil--dragging',
  53. handlersClasses: {
  54. default: 'cropper-handler',
  55. eastNorth: 'cropper-handler--east-north',
  56. westNorth: 'cropper-handler--west-north',
  57. eastSouth: 'cropper-handler--east-south',
  58. westSouth: 'cropper-handler--west-south',
  59. },
  60. }"
  61. :transitions="false"
  62. class="cropper !max-h-[340px] !max-w-[476px]"
  63. background-class="cropper-background"
  64. image-class="cropper__image"
  65. @change="imageCropped"
  66. />
  67. </div>
  68. </CommonFlyout>
  69. </template>
  70. <style scoped>
  71. :deep(.cropper-background) {
  72. background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
  73. }
  74. :deep(.cropper__image) {
  75. opacity: 1;
  76. }
  77. :deep(.cropper-stencil__preview) {
  78. &::after,
  79. &::before {
  80. content: '';
  81. opacity: 0;
  82. transition: opacity 0.25s;
  83. position: absolute;
  84. pointer-events: none;
  85. z-index: 1;
  86. }
  87. &::after {
  88. border-left: solid 1px var(--color-white);
  89. border-right: solid 1px var(--color-white);
  90. width: 33%;
  91. height: 100%;
  92. transform: translateX(-50%);
  93. left: 50%;
  94. top: 0;
  95. }
  96. &::before {
  97. border-top: solid 1px var(--color-white);
  98. border-bottom: solid 1px var(--color-white);
  99. height: 33%;
  100. width: 100%;
  101. transform: translateY(-50%);
  102. top: 50%;
  103. left: 0;
  104. }
  105. }
  106. :deep(.cropper-stencil--dragging .cropper-stencil__preview::after),
  107. :deep(.cropper-stencil--dragging .cropper-stencil__preview::before) {
  108. opacity: 0.4;
  109. }
  110. :deep(.cropper-line) {
  111. border-color: rgba(255, 255, 255, 0.8);
  112. }
  113. :deep(.cropper-handler) {
  114. display: block;
  115. opacity: 0.7;
  116. position: relative;
  117. flex-shrink: 0;
  118. transition: opacity 0.5s;
  119. border: none;
  120. background: white;
  121. top: auto;
  122. left: auto;
  123. height: 4px;
  124. width: 4px;
  125. }
  126. :deep(.cropper-handler--west-north),
  127. :deep(.cropper-handler--east-south),
  128. :deep(.cropper-handler--west-south),
  129. :deep(.cropper-handler--east-north) {
  130. display: block;
  131. height: 16px;
  132. width: 16px;
  133. background: none;
  134. }
  135. :deep(.cropper-handler--west-north) {
  136. border-left: solid 2px var(--color-white);
  137. border-top: solid 2px var(--color-white);
  138. top: 7px;
  139. left: 7px;
  140. }
  141. :deep(.cropper-handler--east-south) {
  142. border-right: solid 2px var(--color-white);
  143. border-bottom: solid 2px var(--color-white);
  144. top: -7px;
  145. left: -7px;
  146. }
  147. :deep(.cropper-handler--west-south) {
  148. border-left: solid 2px var(--color-white);
  149. border-bottom: solid 2px var(--color-white);
  150. top: -7px;
  151. left: 7px;
  152. }
  153. :deep(.cropper-handler--east-north) {
  154. border-right: solid 2px var(--color-white);
  155. border-top: solid 2px var(--color-white);
  156. top: 7px;
  157. left: -7px;
  158. }
  159. :deep(.cropper-handler--hover) {
  160. opacity: 1;
  161. }
  162. </style>