DefaultMediaPicker.vue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <template>
  2. <div class="default-media-picker" :class="rootClass">
  3. <div v-if="labelOutside && label" class="default-media-picker__label">
  4. {{ label }}<span v-if="required" class="text-negative"> *</span>
  5. </div>
  6. <q-file
  7. v-model="model"
  8. v-bind="$attrs"
  9. :accept="accept"
  10. :class="{ 'required-field': required }"
  11. :dense="dense"
  12. :error="!!error"
  13. :error-message="errorMessage"
  14. :label="labelOutside ? undefined : label"
  15. :placeholder="placeholder"
  16. :rules="rules"
  17. lazy-rules="ondemand"
  18. outlined
  19. @update:model-value="onFileChange"
  20. >
  21. <template #prepend>
  22. <slot name="prepend">
  23. <q-icon name="attach_file" />
  24. </slot>
  25. </template>
  26. </q-file>
  27. <div
  28. v-if="showPreview"
  29. class="default-media-picker__preview"
  30. :style="{ height: `${previewHeight}px` }"
  31. >
  32. <img
  33. v-if="previewUrl && isImage"
  34. alt="Pré-visualização do arquivo"
  35. class="default-media-picker__image"
  36. :src="previewUrl"
  37. />
  38. <video
  39. v-else-if="previewUrl && isVideo"
  40. class="default-media-picker__video"
  41. controls
  42. :src="previewUrl"
  43. />
  44. <q-icon
  45. v-else-if="model"
  46. color="grey-6"
  47. name="mdi-file-check-outline"
  48. size="48px"
  49. />
  50. <span v-else class="text-caption text-grey-6">
  51. Nenhum arquivo selecionado
  52. </span>
  53. </div>
  54. </div>
  55. </template>
  56. <script setup>
  57. import { computed, onUnmounted, ref, watch, useAttrs } from "vue";
  58. defineOptions({ inheritAttrs: false });
  59. const {
  60. accept,
  61. dense,
  62. label,
  63. labelOutside,
  64. placeholder,
  65. previewHeight,
  66. rules,
  67. showPreview,
  68. } = defineProps({
  69. accept: { type: String, default: "image/*,video/*,.pdf" },
  70. dense: { type: Boolean, default: false },
  71. label: { type: String, default: "Arquivo" },
  72. labelOutside: { type: Boolean, default: true },
  73. placeholder: { type: String, default: "" },
  74. previewHeight: { type: Number, default: 200 },
  75. rules: { type: Array, default: () => [] },
  76. showPreview: { type: Boolean, default: false },
  77. });
  78. const attrs = useAttrs();
  79. const model = defineModel({ type: [File, null], default: null });
  80. const error = defineModel("error", {
  81. type: [String, Object, Array, Boolean, null],
  82. default: null,
  83. });
  84. const previewUrl = defineModel("previewUrl", { type: String, default: null });
  85. const localObjectUrl = ref(null);
  86. const errorMessage = computed(() =>
  87. typeof error.value === "string" ? error.value : "",
  88. );
  89. const isImage = computed(
  90. () => model.value?.type?.startsWith("image/") ?? false,
  91. );
  92. const isVideo = computed(
  93. () => model.value?.type?.startsWith("video/") ?? false,
  94. );
  95. const required = computed(() => rules.some((rule) => rule?.$id === "required"));
  96. const rootClass = computed(() => attrs.class);
  97. const revokeObjectUrl = () => {
  98. if (localObjectUrl.value) {
  99. URL.revokeObjectURL(localObjectUrl.value);
  100. localObjectUrl.value = null;
  101. }
  102. }
  103. const onFileChange = (file) => {
  104. revokeObjectUrl();
  105. if (file instanceof File && (isImage.value || isVideo.value)) {
  106. localObjectUrl.value = URL.createObjectURL(file);
  107. previewUrl.value = localObjectUrl.value;
  108. } else {
  109. previewUrl.value = null;
  110. }
  111. }
  112. watch(model, (file) => onFileChange(file));
  113. onUnmounted(() => {
  114. revokeObjectUrl();
  115. });
  116. </script>
  117. <style scoped>
  118. .default-media-picker__preview {
  119. display: flex;
  120. align-items: center;
  121. justify-content: center;
  122. width: 100%;
  123. margin-top: 8px;
  124. overflow: hidden;
  125. border: 1px solid #e0e0e0;
  126. border-radius: 4px;
  127. background: #fafafa;
  128. }
  129. .default-media-picker__image,
  130. .default-media-picker__video {
  131. width: 100%;
  132. height: 100%;
  133. object-fit: contain;
  134. }
  135. </style>