DefaultFilePicker.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <template>
  2. <q-field v-model="model" v-bind="$attrs" borderless :rules="rules">
  3. <div class="column flex-center q-mb-sm full-width">
  4. <span class="text-grey-6">{{ label }}</span>
  5. <div
  6. class="image-preview-container"
  7. :class="{
  8. 'has-image': preview,
  9. 'is-dragging': isDragging,
  10. }"
  11. @click="pickImage"
  12. @dragover.prevent="handleDragOver"
  13. @dragleave.prevent="handleDragLeave"
  14. @drop.prevent="handleDrop"
  15. >
  16. <template v-if="!preview">
  17. <q-icon
  18. :name="
  19. isDragging
  20. ? 'file_upload'
  21. : type == 'image'
  22. ? 'add_photo_alternate'
  23. : 'insert_drive_file'
  24. "
  25. size="48px"
  26. color="grey-6"
  27. class="absolute-center"
  28. />
  29. <div
  30. class="text-caption text-grey-6 text-center absolute-bottom q-pb-sm q-px-md"
  31. >
  32. {{
  33. isDragging
  34. ? $t("common.ui.file.drag_and_drop")
  35. : type == "image"
  36. ? $t("common.ui.file.click_select_image")
  37. : $t("common.ui.file.click_select")
  38. }}
  39. </div>
  40. </template>
  41. <q-img
  42. v-else-if="type == 'image'"
  43. :src="preview"
  44. fit="cover"
  45. class="full-height"
  46. >
  47. <div class="absolute-bottom text-right">
  48. <q-btn
  49. flat
  50. dense
  51. round
  52. color="negative"
  53. icon="delete"
  54. @click.stop="clearImage"
  55. />
  56. </div>
  57. </q-img>
  58. <div v-else class="position-relative column full-height flex-center">
  59. <q-icon name="mdi-file-check" size="48px" color="grey-6" />
  60. <div
  61. class="absolute-bottom text-caption text-grey-6 text-center q-mb-sm q-px-md"
  62. >
  63. {{ preview }}
  64. </div>
  65. </div>
  66. </div>
  67. <q-file v-show="false" ref="fileInput" v-model="model" :accept="accept" />
  68. </div>
  69. </q-field>
  70. </template>
  71. <script setup>
  72. import { ref, useTemplateRef, watch } from "vue";
  73. const { label, rules, accept, type, initialImage } = defineProps({
  74. label: {
  75. type: String,
  76. default: "Select Image",
  77. },
  78. rules: {
  79. type: Array,
  80. default: () => [],
  81. },
  82. accept: {
  83. type: String,
  84. default: "image/*",
  85. },
  86. type: {
  87. type: String,
  88. default: "image",
  89. },
  90. initialImage: {
  91. type: String,
  92. default: null,
  93. },
  94. });
  95. const model = defineModel();
  96. const base64File = defineModel("base64File", { type: String, default: null });
  97. const preview = ref(initialImage);
  98. const isDragging = ref(false);
  99. const fileInput = useTemplateRef("fileInput");
  100. const processFile = async (file) => {
  101. if (!file) {
  102. console.error("No file provided");
  103. return;
  104. }
  105. if (type == "image" && !file.type.startsWith("image/")) {
  106. console.error("Invalid file type");
  107. return;
  108. }
  109. if (type == "file") {
  110. const blob = new Blob([file], { type: file.type });
  111. preview.value = file.name;
  112. return new Promise((resolve) => {
  113. const reader = new FileReader();
  114. reader.onload = (e) => {
  115. base64File.value = e.target.result;
  116. console.log(preview.value);
  117. resolve();
  118. };
  119. reader.readAsDataURL(blob);
  120. });
  121. } else {
  122. return new Promise((resolve) => {
  123. const reader = new FileReader();
  124. reader.onload = (e) => {
  125. base64File.value = e.target.result;
  126. preview.value = e.target.result;
  127. resolve();
  128. };
  129. reader.readAsDataURL(file);
  130. });
  131. }
  132. };
  133. const pickImage = () => {
  134. fileInput.value?.pickFiles();
  135. };
  136. const clearImage = () => {
  137. model.value = null;
  138. preview.value = null;
  139. };
  140. const handleDragOver = () => {
  141. isDragging.value = true;
  142. };
  143. const handleDragLeave = () => {
  144. isDragging.value = false;
  145. };
  146. const handleDrop = async (event) => {
  147. isDragging.value = false;
  148. const file = event.dataTransfer?.files?.[0];
  149. if (file) {
  150. model.value = file;
  151. await processFile(file);
  152. }
  153. };
  154. watch(
  155. () => model.value,
  156. async (value, oldValue) => {
  157. if (value != oldValue) {
  158. await processFile(value);
  159. } else {
  160. preview.value = null;
  161. }
  162. },
  163. );
  164. </script>
  165. <style lang="scss" scoped>
  166. @use "sass:map";
  167. @use "src/css/quasar.variables.scss";
  168. .image-preview-container {
  169. .body--dark & {
  170. --image-bg-color: #{map.get($colors-dark, "surface")};
  171. --image-border-color: #{map.get($colors-dark, "primary")};
  172. --image-border-hover-color: #{map.get($colors-dark, "primary-dark")};
  173. }
  174. .body--light & {
  175. --image-bg-color: #{map.get($colors, "surface")};
  176. --image-border-color: #{map.get($colors, "primary")};
  177. --image-border-hover-color: #{map.get($colors, "primary-dark")};
  178. }
  179. width: 200px;
  180. height: 200px;
  181. border: 2px dashed var(--image-border-color);
  182. border-radius: 4px;
  183. position: relative;
  184. overflow: hidden;
  185. transition: all 0.3s;
  186. cursor: pointer;
  187. &.is-dragging {
  188. border-color: var(--image-border-hover-color);
  189. background-color: var(--image-bg-color);
  190. opacity: 0.8;
  191. }
  192. &:hover {
  193. border-color: var(--image-border-hover-color);
  194. background-color: var(--image-bg-color);
  195. }
  196. &.has-image {
  197. border-style: solid;
  198. }
  199. }
  200. </style>