Created
July 15, 2019 15:52
-
-
Save erikfig/a3a1f30b205e3a7a98167ca9f7b28527 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Magic Crud | |
## Instalação | |
Copie os arquivos de acordo com a estrutura padrão do Quasar. | |
No futuro vou transformar em um pacote npm, mas eu chego lá. | |
## Como usar | |
Existem 2 vue components que são configurados para gerar o CRUD | |
- crud-list - página inicial de listagem | |
- crud-form - detalhes, edição e criação | |
### Rotas | |
As rotas são parte importante do processo, precisamos manter um padrão para que o crud possa permitir a navegação de forma correta | |
``` | |
{ | |
path: '/rota-base', | |
component: () => import('layouts/MyLayout.vue'), | |
children: [ | |
{ path: '', component: () => import('pages/diretorio-dos-componentes-deste-moddulo/list.vue') }, | |
{ path: 'novo', component: () => import('pages/diretorio-dos-componentes-deste-moddulo/create.vue') }, | |
{ path: 'detalhes/:id', component: () => import('pages/diretorio-dos-componentes-deste-moddulo/details.vue') }, | |
{ path: 'editar/:id', component: () => import('pages/diretorio-dos-componentes-deste-moddulo/edit.vue') }, | |
], | |
}, | |
``` | |
### Listagem | |
Cria página de listagem de registros com paginação, botão de cadastro de novo registro e botões de ação por registro (ver, editar, remover e ativar/desativar). | |
Parâmetros: | |
- icon: String - Icone do título | |
- title: String - Título da página | |
- title-field: String - Define o campo que exibe o título do registro (exibido na ação de remover) | |
- label-btnNew: String - Personaliza o título do botão NOVO | |
- base-route: String - Qual a rota base para navegação e redirecionamentos (ver **Rotas**, acima) | |
- columns: Array - Colunas - [ver documentação do componente Table](https://quasar.dev/vue-components/table#Defining-the-columns) - também foi adicionado um novo recurso, para customizar o valor a ser exibido, ele se chama `filter`. | |
- remove: Function - Ação de remoção, executada após confirmar a remoção na caixa de diálogo (recebe o id do registro) | |
- visibility-callback: Function - Ação de ativação, desativação de registro, executada ao clicar no botão de ação do registro (recebe um objeto com os dados do registro) | |
- data: Array - Os dados vindos do servidor. | |
O campo title-field pode receber apenas o campo alvo, pode receber uma lista de opções (caso o valor não seja encontrado no primeiro campo, ele busca no seguinte e por ai vai), além disso, para definir um campo título em um relacionamento, basta usar `:`. | |
Exemplo: | |
``` | |
<template> | |
<crud-list | |
title="Usuários PF ou PJ" | |
title-field="pf:nome_curto|pj:nome_fantasia" | |
icon="supervised_user_circle" | |
base-route="/rota-base" | |
:columns="columns" | |
:data="tableData" | |
:visibility-callback="visibilityCallback" | |
:remove="remove"/> | |
</template> | |
<script> | |
export default { | |
data() { | |
return { | |
columns: [ | |
{ | |
name: 'id', | |
label: 'id', | |
field: 'id', | |
sortable: true, | |
}, | |
{ | |
name: 'pf', | |
label: 'Razão Social/Nome Completo', | |
field: 'pf', | |
sortable: true, | |
filter(value, data) { | |
console.log(value, data); | |
if (value) { | |
return value.nome_completo; | |
} | |
return data.pf.razao_social; | |
}, | |
}, | |
{ | |
name: 'pj', | |
label: 'Nome Fantasia/Nome Reduzido', | |
field: 'pj', | |
sortable: true, | |
filter(value, data) { | |
if (value) { | |
return value.nome_fantasia; | |
} | |
return data.pf.nome_curto; | |
}, | |
}, | |
], | |
}; | |
}, | |
computed: { | |
tableData() { | |
return this.$store.state.modulo.list.data; | |
}, | |
}, | |
methods: { | |
async remove(id) { | |
await this.$store.dispatch('modulo/remove', id); | |
this.$q.notify({ | |
message: 'Removido com sucesso', | |
color: 'positive', | |
icon: 'sentiment_very_satisfied', | |
}); | |
this.$store.dispatch('modulo/all'); | |
}, | |
async visibilityCallback(data) { | |
console.log(data); | |
await this.$store.dispatch('modulo/visibility', { id: data.id, active: data.active }); | |
this.$store.dispatch('modulo/all'); | |
}, | |
}, | |
mounted() { | |
this.$store.dispatch('modulo/all'); | |
}, | |
}; | |
</script> | |
<style> | |
</style> | |
``` | |
### Detalhes, edição e criação | |
Todas as 3 telas são criadas com base no component form, a grande diferença entre as páginas de edição e criação é que na primeira, passamos os dados do formulário, já a tela de detalhes recebe um parâmetro que desativa os campos do formulário, o submit e remove o botão de salvamento. | |
- icon: String - Icone do título | |
- title: String - Título da página | |
- form-title: String - Título no campo de formulário | |
- base-route: String - Qual a rota base para navegação e redirecionamentos (ver **Rotas**, acima) | |
- fields: Array - Array de campos do formulário (veja a seguir) | |
- v-model: Object - Os dados a serem exibidos no formulário | |
- submit: Function - O que acontece ao salvar o formulário e depois de todos os campos validados | |
- details: Boolean - Ativa a exibição de detalhes (falso, por padrão) | |
- v-model: Object - Dados para o formulário | |
O grande segredo aqui são os `fields`, eles definem quais campos seu formulário terá, como irá se parecer, e até as validações. Ele deve ser representado por um array de objetos, e cada objeto pode ter os seguintes parâmetros: | |
- field: String - Nome do campo (enviado para o Servidor ou quando requisitado) | |
- label: String - Etiqueta do campo | |
- type: String - Tipo do campo, podendo ser `select`, `state` (lista de estados), `bank` (lista de bancos), `data` (escolher data) e `text` (campo comum) | |
- options: Array - No caso do campo select, ele define as opções que podem ser selecionados, cada item recebe um `value` com o valor e o `label` com o texto a ser exibido. | |
- class: String - Classe para personalizar a exibição, o foco seria definir tamanhos de coluna para cada campo e assim definir tamanhos (largura) ou posicionar mais de um campo por 🇱inha, valor padrão é `col-12`. | |
- visibility: Function - Regra para exibição de um campo, recebe um objeto com os valores do model, deve retornar true ou false (retorna true por padrão) | |
- rules: Array - Regras de validação ([veja documentação oficial](https://quasar.dev/vue-components/input#Validation)) | |
- mask: String - Mascara do campo ([veja documentação oficial](https://quasar.dev/vue-components/input#Mask)) | |
- hint: String - Texto de ajuda para o campo | |
Exemplos: | |
``` | |
// form_fields.js | |
import axios from 'axios'; | |
import validations from '../../validations'; | |
const uniqueCpf = v => axios.get(`/api/real-state-subsidiaries/check-cpf/${v}`); | |
const uniqueCnpj = v => axios.get(`/api/real-state-subsidiaries/check-cnpj/${v}`); | |
export default [ | |
{ | |
field: 'regime_juridico', | |
label: 'Regime jurídico', | |
type: 'select', | |
rules: [validations.required], | |
options: [ | |
{ | |
label: 'Pessoa Física', | |
value: 'F', | |
}, | |
{ | |
label: 'Pessoa Jurídica', | |
value: 'J', | |
}, | |
], | |
class: 'col-6', | |
}, | |
{ | |
field: 'nome_completo', | |
label: 'Nome completo', | |
visibility: data => data.regime_juridico === 'F', | |
rules: [validations.required], | |
}, | |
{ | |
field: 'nome_curto', | |
label: 'Nome curto', | |
visibility: data => data.regime_juridico === 'F', | |
rules: [validations.required], | |
}, | |
{ | |
field: 'cpf', | |
label: 'CPF', | |
mask: '###.###.###-##', | |
visibility: data => data.regime_juridico === 'F', | |
rules: [validations.cpfCnpj, uniqueCpf], | |
}, | |
{ | |
field: 'identidade', | |
label: 'RG', | |
visibility: data => data.regime_juridico === 'F', | |
class: 'col-6', | |
}, | |
{ | |
field: 'orgao_emissor_identidade', | |
label: 'Orgão emissor', | |
visibility: data => data.regime_juridico === 'F', | |
class: 'col-3', | |
}, | |
{ | |
field: 'data_emissao_identidade', | |
label: 'Data de emissão', | |
type: 'date', | |
visibility: data => data.regime_juridico === 'F', | |
class: 'col-3', | |
}, | |
{ | |
field: 'razao_social', | |
label: 'Razão social', | |
visibility: data => data.regime_juridico === 'J', | |
rules: [validations.required], | |
}, | |
{ | |
field: 'nome_fantasia', | |
label: 'Nome fantasia', | |
visibility: data => data.regime_juridico === 'J', | |
rules: [validations.required], | |
}, | |
{ | |
field: 'cnpj', | |
label: 'CNPJ', | |
mask: '##.###.###/####-##', | |
visibility: data => data.regime_juridico === 'J', | |
rules: [validations.cpfCnpj, uniqueCnpj], | |
}, | |
{ | |
field: 'inscricao_estadual', | |
label: 'Inscrição estadual', | |
visibility: data => data.regime_juridico === 'J', | |
}, | |
{ | |
field: 'inscricao_municipal', | |
label: 'Inscrição municipal', | |
visibility: data => data.regime_juridico === 'J', | |
}, | |
{ | |
field: 'cep', | |
label: 'CEP', | |
mask: '########', | |
rules: [validations.cep], | |
}, | |
{ | |
field: 'logradouro', | |
label: 'Logradouro', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
}, | |
{ | |
field: 'numero', | |
label: 'Número', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
}, | |
{ | |
field: 'complemento', | |
label: 'Complemento', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
}, | |
{ | |
field: 'bairro', | |
label: 'Bairro', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
}, | |
{ | |
field: 'municipio', | |
label: 'Município', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
}, | |
{ | |
field: 'estado', | |
label: 'Estado', | |
type: 'state', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
}, | |
{ | |
field: 'telefone1', | |
label: 'Telefone 1', | |
mask: '+## (##) #####-####', | |
hint: 'Com DDI, exemplo: +55 (11) 98888-8888 - DDI do Brasil: 55', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.required], | |
class: 'col-6', | |
}, | |
{ | |
field: 'telefone2', | |
label: 'Telefone 2', | |
mask: '+## (##) #####-####', | |
hint: 'Com DDI, exemplo: +55 (11) 98888-8888 - DDI do Brasil: 55', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
class: 'col-6', | |
}, | |
{ | |
field: 'email', | |
label: 'E-mail', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.email], | |
}, | |
{ | |
field: 'site', | |
label: 'Site', | |
visibility(data) { | |
return data.cep ? data.cep.length === 8 : false; | |
}, | |
rules: [validations.url], | |
}, | |
]; | |
``` | |
``` | |
// details.vue | |
<template> | |
<crud-form | |
:details="true" | |
title="Usuários" | |
icon="supervised_user_circle" | |
base-route="/rota-base" | |
:fields="fields" | |
v-model="data"/> | |
</template> | |
<script> | |
import formFields from './form_fields'; | |
export default { | |
data() { | |
return { | |
fields: formFields, | |
}; | |
}, | |
computed: { | |
data() { | |
return this.$store.state.modulo.first; | |
}, | |
}, | |
mounted() { | |
this.$store.dispatch('modulo/one', this.$route.params.id); | |
}, | |
}; | |
</script> | |
<style> | |
</style> | |
``` | |
``` | |
// edit.vue | |
<template> | |
<crud-form | |
formTitle="Editando Registro" | |
title="Registros" | |
icon="supervised_user_circle" | |
base-route="/rota-base" | |
:fields="fields" | |
:submit="submit" | |
v-model="tableData"/> | |
</template> | |
<script> | |
import formFields from './form_fields'; | |
export default { | |
data() { | |
return { | |
fields: formFields, | |
}; | |
}, | |
computed: { | |
tableData() { | |
return this.$store.state.modulo.first; | |
}, | |
}, | |
methods: { | |
submit(data) { | |
this.$store.dispatch('modulo/update', { data, id: this.$route.params.id }) | |
.then(() => { | |
this.$q.notify({ | |
message: 'Salvo com sucesso', | |
color: 'positive', | |
icon: 'sentiment_very_satisfied', | |
}); | |
this.$router.push('/rota-base'); | |
}) | |
.catch((error) => { | |
const { status } = error.response; | |
const message = `Os dados não foram salvos, verifique as informações! <em>Erro ${status}</em>`; | |
this.$q.notify({ | |
message, | |
html: true, | |
color: 'negative', | |
icon: 'sentiment_very_dissatisfied', | |
}); | |
}); | |
}, | |
}, | |
mounted() { | |
this.$store.dispatch('modulo/one', this.$route.params.id); | |
}, | |
}; | |
</script> | |
<style> | |
</style> | |
``` | |
``` | |
<template> | |
<crud-form | |
formTitle="Cadastrando Registro" | |
title="Registros" | |
icon="supervised_user_circle" | |
base-route="/rota-base" | |
:fields="fields" | |
v-model="data" | |
:submit="submit"/> | |
</template> | |
<script> | |
import formFields from './form_fields'; | |
export default { | |
data() { | |
return { | |
data: {}, | |
fields: formFields, | |
}; | |
}, | |
methods: { | |
submit(data) { | |
this.$store.dispatch('modulo/create', data) | |
.then(() => { | |
this.$q.notify({ | |
message: 'Salvo com sucesso', | |
color: 'positive', | |
icon: 'sentiment_very_satisfied', | |
}); | |
this.$router.push('/rota-base'); | |
}) | |
.catch((error) => { | |
const { status } = error.response; | |
const message = `Os dados não foram salvos, verifique as informações! <em>Erro ${status}</em>`; | |
this.$q.notify({ | |
message, | |
html: true, | |
color: 'negative', | |
icon: 'sentiment_very_dissatisfied', | |
}); | |
}); | |
}, | |
}, | |
}; | |
</script> | |
<style> | |
</style> | |
``` | |
### Considerações | |
Ainda está em desenvolvimento e está é a primeira documentação, espero facilitar o uso cada vez mais, qualquer consideração, por favor, me comunique. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment