فهرست منبع

✨ feat(components): adicionar e atualizar componentes padrão de UI

Fase: dev | Origin: melhoria-interna
Gustavo Zanatta 1 هفته پیش
والد
کامیت
2a542eac44

+ 9 - 8
src/components/defaults/DefaultCurrencyInput.vue

@@ -3,7 +3,7 @@
     ref="inputRef"
     v-model="formattedValue"
     v-bind="$attrs"
-    :label="label"
+    :label="displayLabel"
     :rules="finalRules"
     :input-class="inputClass"
   />
@@ -13,18 +13,16 @@
 import { watch, onBeforeMount, ref } from "vue";
 import { useCurrencyInput } from "vue-currency-input";
 import { useI18n } from "vue-i18n";
-import { useInputRules } from "src/composables/useInputRules";
 
 import DefaultInput from "./DefaultInput.vue";
 
-const { inputRules } = useInputRules();
+const { t } = useI18n();
 
 const model = defineModel({ type: Number });
 
-const defaultRules = [inputRules.minValue(0)];
 const finalRules = ref([]);
 
-const { options, label, rules } = defineProps({
+const props = defineProps({
   options: {
     type: Object,
     default: () => ({
@@ -41,7 +39,7 @@ const { options, label, rules } = defineProps({
   },
   label: {
     type: String,
-    default: useI18n().t("common.terms.currency"),
+    default: null,
   },
   errorMessage: {
     type: String,
@@ -57,6 +55,9 @@ const { options, label, rules } = defineProps({
   },
 });
 
+const displayLabel = props.label ?? t("common.terms.currency");
+const { options, rules, inputClass } = props;
+
 const { inputRef, formattedValue, numberValue, setValue } =
   useCurrencyInput(options);
 
@@ -77,11 +78,11 @@ watch(
 watch(
   () => rules,
   (value) => {
-    finalRules.value = [...value, ...defaultRules];
+    finalRules.value = [...value];
   },
 );
 
 onBeforeMount(() => {
-  finalRules.value = [...rules, ...defaultRules];
+  finalRules.value = [...rules];
 });
 </script>

+ 101 - 137
src/components/defaults/DefaultFilePicker.vue

@@ -14,7 +14,7 @@
       <span v-if="required" class="text-negative q-ml-xs">*</span>
     </div>
     <q-field
-      v-model="model"
+      :model-value="preview"
       v-bind="inputAttrs"
       borderless
       hide-bottom-space
@@ -24,7 +24,7 @@
       class="image-preview-container"
     >
       <div
-        class=""
+        class="file-picker-inner"
         :class="{
           'has-image': preview,
           'is-dragging': isDragging,
@@ -43,51 +43,35 @@
             color="grey-6"
             class="absolute-center"
           />
-          <div
-            class="text-caption text-grey-6 text-center absolute-bottom q-pb-sm q-px-md"
-          >
+          <div class="text-caption text-grey-6 text-center absolute-bottom q-pb-sm q-px-md">
             {{
               isDragging
                 ? $t("common.ui.file.drag_and_drop")
-                : type == "image"
+                : type === "image"
                   ? $t("common.ui.file.click_select_image")
                   : $t("common.ui.file.click_select")
             }}
           </div>
         </template>
 
-        <q-img
+        <div
           v-else-if="type === 'image'"
-          :src="preview"
-          fit="cover"
-          class="full-height"
+          class="file-picker-image"
+          :style="{ backgroundImage: `url('${preview}')` }"
         />
 
-        <div v-else class="position-relative column full-height flex-center">
+        <div v-else class="column flex-center" style="height: 100%">
           <q-icon name="mdi-file-check" size="48px" color="grey-6" />
-          <div
-            class="absolute-bottom text-caption text-grey-6 text-center q-mb-sm q-px-md"
-          >
-            {{ model.name }}
+          <div class="text-caption text-grey-6 text-center q-mt-sm q-px-md">
+            {{ internalFile?.name }}
           </div>
         </div>
-
-        <div v-if="preview" class="absolute-top-right q-ma-xs">
-          <q-btn
-            flat
-            dense
-            round
-            color="negative"
-            icon="mdi-close"
-            @click.stop="clearFile"
-          />
-        </div>
       </div>
 
       <q-file
         v-show="false"
         ref="fileInputRef"
-        v-model="model"
+        v-model="internalFile"
         :accept="accept"
       />
     </q-field>
@@ -95,21 +79,13 @@
 </template>
 
 <script setup>
-import {
-  ref,
-  watch,
-  onUnmounted,
-  useTemplateRef,
-  useAttrs,
-  computed,
-  onBeforeMount,
-} from "vue";
+import { ref, watch, computed, onUnmounted, useTemplateRef, useAttrs } from "vue";
 
 defineOptions({
   inheritAttrs: false,
 });
 
-const { label, rules, accept, type, initialImage } = defineProps({
+const props = defineProps({
   label: {
     type: String,
     default: "Select Image",
@@ -147,117 +123,91 @@ const model = defineModel({ type: [File, String, null], default: null });
 const base64File = defineModel("base64File", { type: String, default: null });
 
 const isDragging = ref(false);
-const preview = ref(initialImage || null);
-const required = ref(false);
+const internalFile = ref(null);
+const objectUrl = ref(null);
 
-let objectUrl = null;
+const required = computed(() => props.rules.some((r) => r?.$id === "required"));
 
-const cleanupObjectURL = () => {
-  if (objectUrl) {
-    URL.revokeObjectURL(objectUrl);
-    objectUrl = null;
+const preview = computed(() => {
+  if (internalFile.value instanceof File) {
+    return props.type === "image" ? objectUrl.value : "file_selected";
   }
-};
+  return props.initialImage || null;
+});
 
-const generateBase64 = (file) => {
-  if (!file) {
-    base64File.value = null;
-    return;
-  }
-  const reader = new FileReader();
-  reader.onload = (e) => {
-    base64File.value = e.target.result;
-  };
-  reader.onerror = () => {
-    console.error("FileReader failed to read file.");
-    base64File.value = null;
-  };
-  reader.readAsDataURL(file);
-};
+const inputAttrs = computed(() => {
+  // eslint-disable-next-line
+  const { class: _, style: __, ...rest } = attrs;
+  return rest;
+});
 
-const pickFile = () => {
+function pickFile() {
   fileInputRef.value?.pickFiles();
-};
-
-const clearFile = () => {
-  model.value = null;
-};
+}
 
-const handleDragOver = (event) => {
+function handleDragOver(event) {
   event.preventDefault();
   event.dataTransfer.dropEffect = "copy";
   isDragging.value = true;
-};
+}
 
-const handleDragLeave = () => {
+function handleDragLeave() {
   isDragging.value = false;
-};
+}
 
-const handleDrop = (event) => {
+function handleDrop(event) {
   event.preventDefault();
   isDragging.value = false;
-
   const file = event.dataTransfer?.files?.[0];
   if (!file) return;
-
-  const acceptedMime = accept;
-
+  const acceptedMime = props.accept;
   if (acceptedMime.endsWith("/*")) {
     const baseMime = acceptedMime.replace("/*", "");
-    if (file.type.startsWith(baseMime + "/")) {
-      model.value = file;
-    }
+    if (file.type.startsWith(baseMime + "/")) internalFile.value = file;
   } else {
-    if (
-      acceptedMime
-        .split(",")
-        .map((m) => m.trim())
-        .includes(file.type)
-    ) {
-      model.value = file;
+    if (acceptedMime.split(",").map((m) => m.trim()).includes(file.type)) {
+      internalFile.value = file;
     }
   }
-};
-
-const inputAttrs = computed(() => {
-  // eslint-disable-next-line
-  const { class: _, style: __, ...rest } = attrs;
-  return rest;
-});
+}
 
-watch(model, (newFile) => {
-  cleanupObjectURL();
+function generateBase64(file) {
+  if (!file) { base64File.value = null; return; }
+  const reader = new FileReader();
+  reader.onload = (e) => { base64File.value = e.target.result; };
+  reader.onerror = () => { base64File.value = null; };
+  reader.readAsDataURL(file);
+}
 
-  if (newFile && newFile instanceof File) {
-    if (type === "image") {
-      objectUrl = URL.createObjectURL(newFile);
-      preview.value = objectUrl;
-    } else {
-      preview.value = "file_selected";
+watch(internalFile, (newFile) => {
+  if (objectUrl.value) {
+    URL.revokeObjectURL(objectUrl.value);
+    objectUrl.value = null;
+  }
+  if (newFile instanceof File) {
+    if (props.type === "image") {
+      objectUrl.value = URL.createObjectURL(newFile);
     }
+    model.value = newFile;
     generateBase64(newFile);
   } else {
-    preview.value = initialImage || null;
+    model.value = null;
     base64File.value = null;
   }
 });
 
-watch(
-  () => rules,
-  (values) => {
-    values.forEach((r) => {
-      if (r?.$id === "required") return (required.value = true);
-    });
-  },
-);
-
-onBeforeMount(() => {
-  rules.forEach((r) => {
-    if (r?.$id === "required") return (required.value = true);
-  });
+watch(model, (val) => {
+  if (!val && internalFile.value) {
+    internalFile.value = null;
+  }
 });
 
-onUnmounted(cleanupObjectURL);
+onUnmounted(() => {
+  if (objectUrl.value) {
+    URL.revokeObjectURL(objectUrl.value);
+    objectUrl.value = null;
+  }
+});
 </script>
 
 <style lang="scss">
@@ -267,46 +217,44 @@ onUnmounted(cleanupObjectURL);
 .image-preview-container {
   display: flex;
   justify-content: center;
-  align-items: center;
+  align-items: stretch;
 
   .q-field__inner {
-    .body--dark & {
-      --image-bg-color: #{map.get($colors-dark, "surface")};
-      --image-border-color: #{map.get($colors-dark, "primary")};
-      --image-border-hover-color: #{map.get($colors-dark, "primary-dark")};
-    }
-
     .body--light & {
       --image-bg-color: #{map.get($colors, "surface")};
-      --image-border-color: #{map.get($colors, "primary")};
-      --image-border-hover-color: #{map.get($colors, "primary-dark")};
+      --image-border-color: #{map.get($colors, "violet-normal")};
+      --image-border-hover-color: #{map.get($colors, "violet-normal-hover")};
     }
 
+    height: 100%;
     display: flex;
     flex-direction: column;
-    transition: all 0.3s;
-
-    &.is-dragging {
-      border-color: var(--image-border-hover-color);
-      background-color: var(--image-bg-color);
-      opacity: 0.8;
-    }
-
-    &.has-image {
-      border-style: solid;
-      margin: auto;
-    }
+    transition: border-color 0.3s;
   }
+
   .q-field__control {
     height: 100%;
     min-height: 200px;
     border: 2px dashed var(--image-border-color);
     border-radius: 8px;
     cursor: pointer;
+    overflow: hidden;
+
+    &:has(.has-image) {
+      border-style: solid;
+      border-color: var(--image-border-color);
+    }
+
+    &:has(.is-dragging) {
+      border-color: var(--image-border-hover-color);
+      opacity: 0.8;
+    }
   }
 
   .q-field__control-container {
-    justify-content: center;
+    height: 100%;
+    justify-content: stretch;
+    align-items: stretch;
   }
 
   .q-field__append {
@@ -314,5 +262,21 @@ onUnmounted(cleanupObjectURL);
     top: -5px;
     right: 10px;
   }
+
+  .file-picker-inner {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    min-height: 196px;
+  }
+
+  .file-picker-image {
+    position: absolute;
+    inset: 0;
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+    border-radius: 6px;
+  }
 }
 </style>

+ 4 - 3
src/components/defaults/DefaultInputDatePicker.vue

@@ -15,9 +15,9 @@
       >
         <q-popup-proxy cover transition-show="scale" transition-hide="scale">
           <template v-if="!time">
-            <q-date v-model="date" mask="YYYY-MM-DD">
+            <q-date v-model="date" mask="YYYY-MM-DD" color="violet-normal">
               <div class="row items-center justify-end">
-                <q-btn v-close-popup label="OK" color="primary" flat />
+                <q-btn v-close-popup label="OK" color="violet-normal" flat />
               </div>
             </q-date>
           </template>
@@ -34,12 +34,13 @@
                 <q-date
                   v-model="date"
                   mask="YYYY-MM-DD HH:mm"
+                  color="violet-normal"
                   @update:model-value="handleDateSelection"
                 />
               </q-tab-panel>
 
               <q-tab-panel name="time" class="q-pa-none">
-                <q-time v-model="date" mask="YYYY-MM-DD HH:mm" format24h />
+                <q-time v-model="date" mask="YYYY-MM-DD HH:mm" format24h color="violet-normal" />
               </q-tab-panel>
             </q-tab-panels>
           </template>

+ 1 - 1
src/components/defaults/DefaultPasswordInput.vue

@@ -6,7 +6,7 @@
   >
      <template #append>
       <q-icon
-        :name="seePassword ? 'mdi-eye-off' : 'mdi-eye'"
+        :name="seePassword ? 'mdi-lock-open-outline' : 'mdi-lock-outline'"
         class="cursor-pointer q-ml-md"
         @click="seePassword = !seePassword"
       />

+ 1 - 1
src/components/defaults/DefaultTabs.vue

@@ -3,7 +3,7 @@
     v-model="tab"
     class="button bg-background-2 text-font"
     indicator-color="transparent"
-    active-color="primary"
+    active-color="violet-normal"
     v-bind="$attrs"
     align="justify"
     active-bg-color="white"