mejoras de UI
This commit is contained in:
@@ -55,17 +55,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<form @submit.prevent="createUser" class="row" style="margin-bottom:10px;">
|
||||
<input v-model="form.username" placeholder="usuario" required class="toggle"/>
|
||||
<input v-model="form.password" placeholder="contraseña" required class="toggle"/>
|
||||
<input v-model="form.vlan" placeholder="VLAN" class="toggle"/>
|
||||
<label class="row toggle" style="gap:6px;"><input type="checkbox" v-model="form.disabled"/> deshabilitado</label>
|
||||
<button type="submit" class="icon-btn">Crear / Actualizar</button>
|
||||
</form>
|
||||
<div v-if="loading.users" class="muted">Cargando usuarios…</div>
|
||||
<div v-else class="grid">
|
||||
<UserCard v-for="u in filteredUsers" :key="u.username" :item="u" :mode="layoutMode"
|
||||
@toggleDisable="toggleDisable" @remove="removeUser" />
|
||||
@toggleDisable="toggleDisable" @remove="removeUser" @edit="openEditUser" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -96,6 +89,11 @@
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal :open="showUserForm" :title="userFormMode==='edit' ? 'Editar usuario' : (userFormMode==='guest' ? 'Agregar invitado' : 'Agregar usuario')"
|
||||
@close="showUserForm=false">
|
||||
<UserForm :model-value="userFormModel" :mode="userFormMode" @submit="handleUserFormSubmit" @cancel="showUserForm=false" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -103,11 +101,12 @@ import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import EventCard from './components/EventCard.js';
|
||||
import UserCard from './components/UserCard.js';
|
||||
import Modal from './components/Modal.vue';
|
||||
import UserForm from './components/UserForm.vue';
|
||||
|
||||
const users = ref([]);
|
||||
const requests = ref([]);
|
||||
const loading = reactive({ users: false, requests: false });
|
||||
const form = reactive({ username: '', password: '', vlan: '', disabled: false });
|
||||
// formulario inline removido: se usa modal con UserForm
|
||||
|
||||
const showEventFilters = ref(false);
|
||||
const showUserFilters = ref(false);
|
||||
@@ -137,17 +136,6 @@ async function fetchRequests() {
|
||||
} finally { loading.requests = false; }
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const payload = { ...form };
|
||||
if (!payload.vlan) delete payload.vlan;
|
||||
await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
form.username = '';
|
||||
form.password = '';
|
||||
form.vlan = '';
|
||||
form.disabled = false;
|
||||
await fetchUsers();
|
||||
}
|
||||
|
||||
async function toggleDisable(u) {
|
||||
await fetch(`/api/users/${encodeURIComponent(u.username)}`, {
|
||||
method: 'PATCH',
|
||||
@@ -227,7 +215,36 @@ function applyTheme() {
|
||||
document.documentElement.classList.toggle('light', theme.value === 'light');
|
||||
}
|
||||
|
||||
function openAddUser() { /* placeholder for advanced modal */ }
|
||||
function openAddGuest() { /* placeholder for advanced modal */ }
|
||||
function openSettings() { /* placeholder for advanced modal */ }
|
||||
const showUserForm = ref(false);
|
||||
const userFormMode = ref('create'); // 'create' | 'edit' | 'guest'
|
||||
const userFormModel = ref({ username:'', password:'', vlan:'', disabled:false });
|
||||
|
||||
function openAddUser() {
|
||||
userFormMode.value = 'create';
|
||||
userFormModel.value = { username:'', password:'', vlan:'', disabled:false };
|
||||
showUserForm.value = true;
|
||||
}
|
||||
function openAddGuest() {
|
||||
userFormMode.value = 'guest';
|
||||
userFormModel.value = { username:'', password:'', vlan:'5', disabled:false };
|
||||
showUserForm.value = true;
|
||||
}
|
||||
function openSettings() { /* modal de configuración futura */ }
|
||||
function openEditUser(u) {
|
||||
userFormMode.value = 'edit';
|
||||
userFormModel.value = { username: u.username, password: u.password || '', vlan: u.vlan || '', disabled: !!u.disabled };
|
||||
showUserForm.value = true;
|
||||
}
|
||||
|
||||
async function handleUserFormSubmit(data) {
|
||||
if (userFormMode.value === 'edit') {
|
||||
await fetch(`/api/users/${encodeURIComponent(userFormModel.value.username)}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
||||
}
|
||||
await fetchUsers();
|
||||
showUserForm.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,16 +5,18 @@ const html = htm.bind(h);
|
||||
export default defineComponent({
|
||||
name: 'UserCard',
|
||||
props: { item: { type: Object, required: true }, mode: { type: String, default: 'user' } },
|
||||
emits: ['toggleDisable', 'remove'],
|
||||
emits: ['toggleDisable', 'remove', 'edit'],
|
||||
setup(props, { emit }) {
|
||||
function toggle() { emit('toggleDisable', props.item); }
|
||||
function remove() { emit('remove', props.item); }
|
||||
function edit() { emit('edit', props.item); }
|
||||
return () => html`<div class="card">
|
||||
<div class="row">
|
||||
<b>${props.mode === 'user' ? props.item.username : (props.item.device || props.item.username)}</b>
|
||||
<span class="chip">VLAN ${props.item.vlan}</span>
|
||||
${props.item.disabled ? html`<span class="chip" style="color:#b33">deshabilitado</span>` : html`<span class="chip">activo</span>`}
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" onClick=${edit}>Editar</button>
|
||||
<button class="icon-btn" onClick=${toggle}>${props.item.disabled ? 'Habilitar' : 'Deshabilitar'}</button>
|
||||
<button class="icon-btn" onClick=${remove}>Eliminar</button>
|
||||
</div>
|
||||
@@ -22,4 +24,3 @@ export default defineComponent({
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
50
frontend/src/components/UserForm.vue
Normal file
50
frontend/src/components/UserForm.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit" class="column" style="gap:10px;">
|
||||
<div class="row">
|
||||
<label class="toggle" style="flex:1;">
|
||||
<div class="muted" style="font-size:12px;">Usuario</div>
|
||||
<input v-model="state.username" :readonly="isEdit" placeholder="usuario" style="width:100%; background:transparent; border:none; outline:none; color:inherit;"/>
|
||||
</label>
|
||||
<label class="toggle" style="flex:1;">
|
||||
<div class="muted" style="font-size:12px;">Contraseña</div>
|
||||
<input v-model="state.password" placeholder="contraseña" style="width:100%; background:transparent; border:none; outline:none; color:inherit;"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="toggle" style="flex:1;">
|
||||
<div class="muted" style="font-size:12px;">VLAN</div>
|
||||
<input v-model="state.vlan" placeholder="VLAN" style="width:100%; background:transparent; border:none; outline:none; color:inherit;"/>
|
||||
</label>
|
||||
<label class="row toggle" style="gap:6px;">
|
||||
<input type="checkbox" v-model="state.disabled"/>
|
||||
Deshabilitado
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="icon-btn" @click="$emit('cancel')">Cancelar</button>
|
||||
<button type="submit" class="icon-btn">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, default: () => ({ username:'', password:'', vlan:'', disabled:false }) },
|
||||
mode: { type: String, default: 'create' } // 'create' | 'edit' | 'guest'
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'cancel']);
|
||||
|
||||
const state = reactive({ username:'', password:'', vlan:'', disabled:false });
|
||||
const isEdit = computed(() => props.mode === 'edit');
|
||||
|
||||
watch(() => props.modelValue, (v) => {
|
||||
Object.assign(state, v || {});
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
function submit() {
|
||||
emit('submit', { ...state });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
--border: 255 255 255 / 0.12;
|
||||
--glass-blur: 14px;
|
||||
--radius: 14px;
|
||||
/* Scrollbar */
|
||||
--sb-size: 10px;
|
||||
--sb-thumb: rgba(255, 159, 203, 0.55);
|
||||
--sb-thumb-hover: rgba(255, 159, 203, 0.75);
|
||||
--sb-thumb-active: rgba(255, 127, 187, 0.9);
|
||||
--sb-track: rgba(255,255,255,0.05);
|
||||
}
|
||||
:root.light {
|
||||
--bg: 245 245 248;
|
||||
@@ -15,6 +21,10 @@
|
||||
--accent: 18 108 242;
|
||||
--card: 255 255 255 / 0.6;
|
||||
--border: 0 0 0 / 0.08;
|
||||
--sb-thumb: rgba(255, 127, 187, 0.65);
|
||||
--sb-thumb-hover: rgba(255, 127, 187, 0.82);
|
||||
--sb-thumb-active: rgba(255, 110, 178, 0.95);
|
||||
--sb-track: rgba(0,0,0,0.06);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { height: 100%; }
|
||||
@@ -28,8 +38,16 @@ a { color: inherit; }
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex; flex-wrap: wrap; align-items: center;
|
||||
gap: 10px; padding: 10px 14px; backdrop-filter: blur(var(--glass-blur));
|
||||
background: linear-gradient(180deg, rgba(var(--card)), rgba(var(--card)) 60%, rgba(0,0,0,0));
|
||||
border-bottom: 1px solid rgba(var(--border));
|
||||
border: 1px solid transparent;
|
||||
background:
|
||||
linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box,
|
||||
linear-gradient(135deg, #ff9fcb, #ff7fbb) border-box;
|
||||
}
|
||||
:root:not(.light) .topbar {
|
||||
background:
|
||||
linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box,
|
||||
linear-gradient(135deg, #ff2e86, #ff6bb0) border-box;
|
||||
box-shadow: 0 0 18px rgba(255, 46, 134, 0.25), 0 0 6px rgba(255, 107, 176, 0.2);
|
||||
}
|
||||
.title { font-size: 16px; font-weight: 700; letter-spacing: .2px; flex: 1 1 auto; }
|
||||
.actions { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
@@ -40,7 +58,17 @@ a { color: inherit; }
|
||||
|
||||
/* Layout */
|
||||
.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; gap: 12px; padding: 12px; }
|
||||
.panel { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
|
||||
.panel { border: 1px solid transparent; background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0;
|
||||
background:
|
||||
linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box,
|
||||
linear-gradient(135deg, #ff9fcb, #ff7fbb) border-box;
|
||||
}
|
||||
:root:not(.light) .panel {
|
||||
background:
|
||||
linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box,
|
||||
linear-gradient(135deg, #ff2e86, #ff6bb0) border-box;
|
||||
box-shadow: 0 0 22px rgba(255, 46, 134, 0.18), 0 0 8px rgba(255, 107, 176, 0.15);
|
||||
}
|
||||
.panel-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid rgba(var(--border)); }
|
||||
.panel-title { font-weight: 600; }
|
||||
.panel-actions { display: inline-flex; flex-wrap: wrap; gap: 6px; }
|
||||
@@ -73,3 +101,33 @@ a { color: inherit; }
|
||||
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.spacer { flex: 1; }
|
||||
.toggle { padding: 6px 10px; border-radius: 8px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); }
|
||||
|
||||
/* Scrollbars */
|
||||
/* Firefox */
|
||||
html, body, .scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--sb-thumb) transparent;
|
||||
}
|
||||
/* WebKit */
|
||||
html::-webkit-scrollbar, body::-webkit-scrollbar, .scroll::-webkit-scrollbar {
|
||||
width: var(--sb-size);
|
||||
height: var(--sb-size);
|
||||
}
|
||||
html::-webkit-scrollbar-track, body::-webkit-scrollbar-track, .scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
html::-webkit-scrollbar-thumb, body::-webkit-scrollbar-thumb, .scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--sb-thumb);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent; /* creates inset padding */
|
||||
background-clip: content-box;
|
||||
box-shadow: 0 0 10px rgba(255, 46, 134, 0.15);
|
||||
}
|
||||
html::-webkit-scrollbar-thumb:hover, body::-webkit-scrollbar-thumb:hover, .scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sb-thumb-hover);
|
||||
background-clip: content-box;
|
||||
}
|
||||
html::-webkit-scrollbar-thumb:active, body::-webkit-scrollbar-thumb:active, .scroll::-webkit-scrollbar-thumb:active {
|
||||
background: var(--sb-thumb-active);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user