adicoonado compoente de dataehora
This commit is contained in:
parent
eca01fca75
commit
fd5c49071c
15 changed files with 1430 additions and 233 deletions
231
src/componentes/data_hora/EliDataHora.vue
Normal file
231
src/componentes/data_hora/EliDataHora.vue
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<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>
|
||||
|
||||
108
src/componentes/data_hora/README.md
Normal file
108
src/componentes/data_hora/README.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# EliDataHora
|
||||
|
||||
O `EliDataHora` é um componente de **entrada de data e hora** baseado em `v-text-field` (Vuetify), usando o tipo nativo do HTML `datetime-local`.
|
||||
|
||||
Ele foi criado para oferecer uma solução **estável e leve** sem depender de componentes experimentais do Vuetify.
|
||||
|
||||
## Objetivo
|
||||
|
||||
- Permitir o usuário selecionar **data + hora** com UX nativa do navegador.
|
||||
- Padronizar a API em português (props/eventos) no Design System.
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Tipo | Padrão | Descrição |
|
||||
|------|------|--------|-----------|
|
||||
| `modelValue` | `string \| null` | `null` | **Sempre em ISO 8601**, aceitando UTC absoluto (`Z`) ou com offset (ex.: `2026-01-09T16:15:00Z`, `2026-01-09T13:15:00-03:00`). O componente converte para horário **local** antes de exibir. |
|
||||
| `modo` | `"data" \| "dataHora"` | `"dataHora"` | Define se o campo permite selecionar apenas data (`date`) ou data+hora (`datetime-local`). |
|
||||
| `rotulo` | `string` | `"Data e hora"` | Label do campo. |
|
||||
| `placeholder` | `string` | `""` | Placeholder do campo. |
|
||||
| `desabilitado` | `boolean` | `false` | Desabilita o campo. |
|
||||
| `limpavel` | `boolean` | `false` | Habilita botão de limpar (Vuetify `clearable`). |
|
||||
| `erro` | `boolean` | `false` | Estado de erro visual. |
|
||||
| `mensagensErro` | `string \| string[]` | `[]` | Mensagens de erro. |
|
||||
| `dica` | `string` | `""` | Hint/ajuda abaixo do campo. |
|
||||
| `dicaPersistente` | `boolean` | `false` | Mantém dica sempre visível. |
|
||||
| `densidade` | `CampoDensidade` | `"comfortable"` | Densidade (Vuetify). |
|
||||
| `variante` | `CampoVariante` | `"outlined"` | Variante (Vuetify). |
|
||||
| `min` | `string \| undefined` | `undefined` | Mínimo permitido em ISO 8601 (offset ou `Z`). |
|
||||
| `max` | `string \| undefined` | `undefined` | Máximo permitido em ISO 8601 (offset ou `Z`). |
|
||||
|
||||
> Observação: o atributo HTML `datetime-local` **não inclui timezone**.
|
||||
> Este componente resolve isso convertendo:
|
||||
>
|
||||
> - **entrada**: ISO 8601 (UTC/offset) → **exibição** em horário local
|
||||
> - **saída**: valor selecionado → ISO 8601 com **offset local**
|
||||
|
||||
### Emits
|
||||
|
||||
| Evento | Payload | Quando dispara |
|
||||
|--------|---------|---------------|
|
||||
| `update:modelValue` | `string \| null` | Sempre que o valor muda (padrão do v-model). O payload é ISO 8601 com **offset local**. |
|
||||
| `alterar` | `string \| null` | Alias semântico para mudanças de valor (mesmo payload do v-model). |
|
||||
| `foco` | `void` | Ao focar o campo. |
|
||||
| `desfoco` | `void` | Ao sair do foco. |
|
||||
|
||||
### Slots
|
||||
|
||||
Este componente não define slots próprios. Você pode usar slots do `v-text-field` via `v-bind="$attrs"` caso precise (ver exemplos abaixo).
|
||||
|
||||
## Exemplos
|
||||
|
||||
### 1) Uso básico com v-model
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<EliDataHora v-model="dataHora" />
|
||||
<div class="text-caption">Valor: {{ dataHora }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { EliDataHora } from "eli-vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { EliDataHora },
|
||||
setup() {
|
||||
const dataHora = ref<string | null>("2026-01-09T16:15:00Z");
|
||||
return { dataHora };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2) Com limites (min/max) e validação visual
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<EliDataHora
|
||||
v-model="dataHora"
|
||||
rotulo="Agendar"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:erro="!dataHora"
|
||||
:mensagensErro="!dataHora ? ['Obrigatório'] : []"
|
||||
limpavel
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Casos de borda / comportamento esperado
|
||||
|
||||
- Ao limpar o campo, o componente emite `null` (não string vazia).
|
||||
- O navegador pode variar a UI do seletor (isso é esperado do `datetime-local`).
|
||||
- `min/max` devem ser strings em ISO 8601 (offset ou `Z`).
|
||||
- Em `modo="data"`, o componente emite ISO no **início do dia** (`00:00:00`) no fuso local.
|
||||
|
||||
## Acessibilidade
|
||||
|
||||
- O `v-text-field` do Vuetify já oferece base de acessibilidade.
|
||||
- Sempre prefira passar `rotulo` significativo.
|
||||
|
||||
## Decisões de implementação
|
||||
|
||||
- Usamos `datetime-local` por ser amplamente suportado e não depender de APIs experimentais.
|
||||
- O componente usa `dayjs` para converter entradas UTC/offset para local antes de exibir e para emitir ISO 8601 com offset local.
|
||||
- Mantemos o valor como `string | null` para evitar conversões implícitas e permitir que cada projeto decida como persistir (UTC/local).
|
||||
1
src/componentes/data_hora/index.ts
Normal file
1
src/componentes/data_hora/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as EliDataHora } from "./EliDataHora.vue";
|
||||
Loading…
Add table
Add a link
Reference in a new issue