DefaultFilePicker.vue 5.4 KB

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