231 lines
5.8 KiB
Vue
231 lines
5.8 KiB
Vue
<template>
|
|
<div class="eli-data-hora">
|
|
<!--
|
|
Implementação propositalmente “simples” e estável:
|
|
- Usa o input nativo `datetime-local` dentro do v-text-field.
|
|
- Evita depender de componentes experimentais (labs) do Vuetify.
|
|
- Mantém v-model como string ISO local: `YYYY-MM-DDTHH:mm`.
|
|
|
|
Observação importante:
|
|
- `datetime-local` NÃO armazena timezone.
|
|
- Se o projeto precisar persistir em UTC, converta no consumidor.
|
|
-->
|
|
<v-text-field
|
|
v-model="valor"
|
|
:type="tipoInput"
|
|
:label="rotulo"
|
|
:placeholder="placeholder"
|
|
:disabled="desabilitado"
|
|
:clearable="limpavel"
|
|
:error="erro"
|
|
:error-messages="mensagensErro"
|
|
:hint="dica"
|
|
:persistent-hint="dicaPersistente"
|
|
:density="densidade"
|
|
:variant="variante"
|
|
:min="minLocal"
|
|
:max="maxLocal"
|
|
v-bind="attrs"
|
|
@focus="emit('foco')"
|
|
@blur="emit('desfoco')"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { computed, defineComponent, PropType } from "vue";
|
|
import dayjs from "dayjs";
|
|
import type { CampoDensidade, CampoVariante } from "../../tipos";
|
|
|
|
/**
|
|
* EliDataHora
|
|
*
|
|
* Campo para entrada de data + hora.
|
|
*
|
|
* Modelo:
|
|
* - O componente **recebe** `modelValue` em ISO 8601 (UTC `Z` ou com offset).
|
|
* - Converte para horário local para exibir no `datetime-local`.
|
|
* - Ao editar, **emite** ISO 8601 com o **offset local**.
|
|
*/
|
|
export default defineComponent({
|
|
name: "EliDataHora",
|
|
inheritAttrs: false,
|
|
|
|
props: {
|
|
/**
|
|
* Valor em ISO 8601:
|
|
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
|
|
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
|
|
*/
|
|
modelValue: {
|
|
type: String as PropType<string | null>,
|
|
default: null,
|
|
},
|
|
|
|
/**
|
|
* Define o tipo de entrada.
|
|
* - `dataHora`: usa `datetime-local`
|
|
* - `data`: usa `date`
|
|
*/
|
|
modo: {
|
|
type: String as PropType<"data" | "dataHora">,
|
|
default: "dataHora",
|
|
},
|
|
|
|
/** Rótulo exibido no v-text-field (Vuetify). */
|
|
rotulo: {
|
|
type: String,
|
|
default: "Data e hora",
|
|
},
|
|
|
|
/** Placeholder do input. */
|
|
placeholder: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
|
|
/** Desabilita a interação. */
|
|
desabilitado: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
|
|
limpavel: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/** Estado de erro (visual). */
|
|
erro: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/** Mensagens de erro. */
|
|
mensagensErro: {
|
|
type: [String, Array] as PropType<string | string[]>,
|
|
default: () => [],
|
|
},
|
|
|
|
/** Texto de apoio. */
|
|
dica: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
|
|
/** Mantém a dica sempre visível. */
|
|
dicaPersistente: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/** Densidade do campo (Vuetify). */
|
|
densidade: {
|
|
type: String as PropType<CampoDensidade>,
|
|
default: "comfortable",
|
|
},
|
|
|
|
/** Variante do v-text-field (Vuetify). */
|
|
variante: {
|
|
type: String as PropType<CampoVariante>,
|
|
default: "outlined",
|
|
},
|
|
|
|
/**
|
|
* Valor mínimo permitido.
|
|
* ISO 8601 (offset ou `Z`).
|
|
*/
|
|
min: {
|
|
// ISO 8601 (offset ou Z)
|
|
type: String as PropType<string | undefined>,
|
|
default: undefined,
|
|
},
|
|
|
|
/**
|
|
* Valor máximo permitido.
|
|
* ISO 8601 (offset ou `Z`).
|
|
*/
|
|
max: {
|
|
// ISO 8601 (offset ou Z)
|
|
type: String as PropType<string | undefined>,
|
|
default: undefined,
|
|
},
|
|
},
|
|
|
|
emits: {
|
|
/** v-model padrão. */
|
|
"update:modelValue": (_valor: string | null) => true,
|
|
/** Alias para consumidores que querem um evento semântico. */
|
|
alterar: (_valor: string | null) => true,
|
|
foco: () => true,
|
|
desfoco: () => true,
|
|
},
|
|
|
|
setup(props, { emit, attrs }) {
|
|
const tipoInput = computed<"date" | "datetime-local">(() =>
|
|
props.modo === "data" ? "date" : "datetime-local"
|
|
);
|
|
|
|
// Converte ISO (Z/offset) para o formato que o `datetime-local` aceita.
|
|
function isoParaInputDatetime(valorIso: string): string {
|
|
// `dayjs(valorIso)` interpreta ISO com timezone e converte para o local do usuário.
|
|
if (props.modo === "data") {
|
|
return dayjs(valorIso).format("YYYY-MM-DD");
|
|
}
|
|
|
|
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm");
|
|
}
|
|
|
|
// Converte o valor do input (`YYYY-MM-DDTHH:mm`) para ISO 8601 com offset local.
|
|
function inputDatetimeParaIsoLocal(valorInput: string): string {
|
|
// `format()` retorna ISO 8601 com offset local.
|
|
// Em modo `data`, normalizamos para o começo do dia (00:00:00) no fuso local.
|
|
if (props.modo === "data") {
|
|
return dayjs(`${valorInput}T00:00`).format();
|
|
}
|
|
|
|
return dayjs(valorInput).format();
|
|
}
|
|
|
|
const valor = computed<string>({
|
|
get: () => {
|
|
if (!props.modelValue) return "";
|
|
return isoParaInputDatetime(props.modelValue);
|
|
},
|
|
set: (v) => {
|
|
// O `datetime-local` entrega string ou "" quando limpo.
|
|
const normalizado = v && v.length > 0 ? v : null;
|
|
if (!normalizado) {
|
|
emit("update:modelValue", null);
|
|
emit("alterar", null);
|
|
return;
|
|
}
|
|
|
|
const valorEmitido = inputDatetimeParaIsoLocal(normalizado);
|
|
emit("update:modelValue", valorEmitido);
|
|
emit("alterar", valorEmitido);
|
|
},
|
|
});
|
|
|
|
const minLocal = computed<string | undefined>(() => {
|
|
if (!props.min) return undefined;
|
|
return isoParaInputDatetime(props.min);
|
|
});
|
|
|
|
const maxLocal = computed<string | undefined>(() => {
|
|
if (!props.max) return undefined;
|
|
return isoParaInputDatetime(props.max);
|
|
});
|
|
|
|
return { attrs, valor, emit, minLocal, maxLocal, tipoInput };
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.eli-data-hora {
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
|