dc04567c48
Replace the 10-color row with a 5-row × 10-column palette covering red, rose, orange, amber, yellow, green, teal, cyan, blue, indigo, violet, purple, fuchsia and gray families. Selected swatch is highlighted with a border ring. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
9.0 KiB
Vue
233 lines
9.0 KiB
Vue
<template>
|
|
<div class="page">
|
|
<div class="page-header">
|
|
<h1>{{ t('networks') }}</h1>
|
|
<button class="btn-primary" @click="openAdd">{{ t('addNetwork') }}</button>
|
|
</div>
|
|
|
|
<div v-if="props.vlans.length === 0" class="empty">
|
|
{{ t('noNetworksConfigured') }}
|
|
</div>
|
|
|
|
<div class="table-wrap" v-else>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t('colType') }}</th>
|
|
<th>{{ t('colName') }}</th>
|
|
<th>{{ t('colSubnet') }}</th>
|
|
<th>{{ t('colColor') }}</th>
|
|
<th>{{ t('colActions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="vlan in props.vlans" :key="vlan.id">
|
|
<td>
|
|
<span class="badge-vlan" :style="{ background: vlan.color }">
|
|
{{ vlan.vlan_id != null ? vlan.vlan_id : 'LAN' }}
|
|
</span>
|
|
</td>
|
|
<td>{{ vlan.name }}</td>
|
|
<td><code>{{ vlan.cidr || '—' }}</code></td>
|
|
<td>
|
|
<div class="color-preview" :style="{ background: vlan.color }"></div>
|
|
</td>
|
|
<td class="actions">
|
|
<button class="btn-icon" @click="openEdit(vlan)" title="✎">✎</button>
|
|
<button class="btn-icon danger" @click="remove(vlan)" title="✕">✕</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div v-if="showForm" class="modal-overlay" @click.self="showForm = false">
|
|
<div class="modal">
|
|
<h2>{{ editing ? t('editNetwork') : t('newNetwork') }}</h2>
|
|
<form @submit.prevent="save">
|
|
<div class="field">
|
|
<label>{{ t('vlanId') }} <span class="optional">{{ t('vlanIdHint') }}</span></label>
|
|
<input v-model="form.vlan_id" type="number" placeholder="ex: 10" />
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ t('fieldName') }}</label>
|
|
<input v-model="form.name" type="text" required placeholder="ex: Serveurs" />
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ t('subnet') }}</label>
|
|
<input v-model="form.cidr" type="text" :placeholder="t('subnetPlaceholder')" />
|
|
</div>
|
|
<div class="field">
|
|
<label>{{ t('color') }}</label>
|
|
<div class="color-picker-row">
|
|
<input v-model="form.color" type="color" />
|
|
<span class="color-hex">{{ form.color }}</span>
|
|
</div>
|
|
<div class="color-presets">
|
|
<div
|
|
v-for="c in presetColors"
|
|
:key="c"
|
|
class="preset"
|
|
:class="{ selected: form.color.toLowerCase() === c.toLowerCase() }"
|
|
:style="{ background: c }"
|
|
:title="c"
|
|
@click="form.color = c"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn-secondary" @click="showForm = false">{{ t('cancel') }}</button>
|
|
<button type="submit" class="btn-primary">{{ editing ? t('save') : t('create') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive } from 'vue'
|
|
import { vlansApi } from '../api.js'
|
|
import { t, tFmt } from '../i18n.js'
|
|
|
|
const props = defineProps({ vlans: Array })
|
|
const emit = defineEmits(['refresh'])
|
|
|
|
const showForm = ref(false)
|
|
const editing = ref(null)
|
|
const form = reactive({ vlan_id: '', name: '', cidr: '', color: '#4A90D9' })
|
|
|
|
const presetColors = [
|
|
// Rouges / Roses
|
|
'#FCA5A5', '#EF4444', '#DC2626', '#991B1B', '#FDA4AF', '#F43F5E', '#E11D48', '#9F1239', '#FBB6CE', '#F472B6',
|
|
// Oranges / Ambers / Jaunes
|
|
'#FED7AA', '#F97316', '#EA580C', '#9A3412', '#FDE68A', '#F59E0B', '#D97706', '#92400E', '#FEF08A', '#EAB308',
|
|
// Verts / Teals
|
|
'#D9F99D', '#84CC16', '#22C55E', '#16A34A', '#BBF7D0', '#10B981', '#059669', '#99F6E4', '#14B8A6', '#0D9488',
|
|
// Cyans / Bleus
|
|
'#BAE6FD', '#0EA5E9', '#0284C7', '#0C4A6E', '#BFDBFE', '#3B82F6', '#2563EB', '#1E3A8A', '#C7D2FE', '#6366F1',
|
|
// Violets / Gris
|
|
'#DDD6FE', '#8B5CF6', '#7C3AED', '#4C1D95', '#E9D5FF', '#A855F7', '#D946EF', '#CBD5E1', '#94A3B8', '#1E293B',
|
|
]
|
|
|
|
function openAdd() {
|
|
editing.value = null
|
|
Object.assign(form, { vlan_id: '', name: '', cidr: '', color: '#4A90D9' })
|
|
showForm.value = true
|
|
}
|
|
|
|
function openEdit(vlan) {
|
|
editing.value = vlan
|
|
Object.assign(form, {
|
|
vlan_id: vlan.vlan_id != null ? String(vlan.vlan_id) : '',
|
|
name: vlan.name,
|
|
cidr: vlan.cidr || '',
|
|
color: vlan.color
|
|
})
|
|
showForm.value = true
|
|
}
|
|
|
|
async function save() {
|
|
const payload = {
|
|
...form,
|
|
vlan_id: form.vlan_id !== '' ? Number(form.vlan_id) : null
|
|
}
|
|
try {
|
|
if (editing.value) {
|
|
await vlansApi.update(editing.value.id, payload)
|
|
} else {
|
|
await vlansApi.create(payload)
|
|
}
|
|
showForm.value = false
|
|
emit('refresh')
|
|
} catch (e) {
|
|
alert(e.response?.data?.detail || t('saveError'))
|
|
}
|
|
}
|
|
|
|
async function remove(vlan) {
|
|
const label = vlan.vlan_id != null ? `VLAN ${vlan.vlan_id} — ${vlan.name}` : `LAN ${vlan.name}`
|
|
if (!confirm(`Supprimer ${tFmt('confirmDeleteNetwork', label)}`)) return
|
|
try {
|
|
await vlansApi.remove(vlan.id)
|
|
emit('refresh')
|
|
} catch (e) {
|
|
alert(e.response?.data?.detail || t('deleteError'))
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page { height: 100vh; overflow-y: auto; background: var(--bg-page); padding: 32px; }
|
|
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
h1 { font-size: 22px; font-weight: 700; color: var(--text-primary); }
|
|
|
|
.btn-primary {
|
|
padding: 9px 18px; background: #3B82F6; color: #fff;
|
|
border: none; border-radius: 8px; font-size: 14px; font-weight: 600; transition: background 0.15s;
|
|
}
|
|
.btn-primary:hover { background: #2563EB; }
|
|
.btn-secondary {
|
|
padding: 9px 18px; background: var(--border); color: var(--text-secondary);
|
|
border: none; border-radius: 8px; font-size: 14px; font-weight: 500;
|
|
}
|
|
.btn-secondary:hover { background: var(--border-strong); }
|
|
|
|
.empty { padding: 48px; text-align: center; color: var(--text-faint); font-size: 15px; }
|
|
|
|
.table-wrap { background: var(--bg-card); border-radius: 12px; box-shadow: var(--shadow-card); overflow: hidden; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
thead { background: var(--bg-thead); }
|
|
th { padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
td { padding: 14px 16px; font-size: 14px; color: var(--text-primary); border-top: 1px solid var(--border); }
|
|
tr:hover td { background: var(--bg-card-hover); }
|
|
|
|
.badge-vlan {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
min-width: 36px; padding: 2px 8px; border-radius: 20px;
|
|
color: #fff; font-weight: 700; font-size: 13px;
|
|
}
|
|
code { background: var(--bg-page); padding: 2px 6px; border-radius: 4px; font-size: 13px; color: var(--text-secondary); }
|
|
.color-preview { width: 24px; height: 24px; border-radius: 6px; border: 2px solid rgba(0,0,0,0.1); }
|
|
|
|
.actions { display: flex; gap: 8px; }
|
|
.btn-icon {
|
|
width: 30px; height: 30px; border: none; border-radius: 6px;
|
|
background: var(--bg-chip); color: var(--text-secondary); font-size: 15px;
|
|
display: flex; align-items: center; justify-content: center; transition: all 0.15s;
|
|
}
|
|
.btn-icon:hover { background: var(--border-strong); color: var(--text-primary); }
|
|
.btn-icon.danger:hover { background: rgba(239,68,68,0.15); color: #EF4444; }
|
|
|
|
.modal-overlay {
|
|
position: fixed; inset: 0; background: var(--modal-overlay);
|
|
display: flex; align-items: center; justify-content: center; z-index: 100;
|
|
}
|
|
.modal {
|
|
background: var(--bg-card); border-radius: 16px; padding: 28px;
|
|
width: 440px; max-width: 95vw; box-shadow: var(--shadow-modal);
|
|
}
|
|
.modal h2 { font-size: 18px; font-weight: 700; color: var(--text-primary); margin-bottom: 20px; }
|
|
|
|
.field { margin-bottom: 16px; }
|
|
.field label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
|
|
.optional { font-weight: 400; color: var(--text-faint); font-size: 12px; }
|
|
.field input[type="text"],
|
|
.field input[type="number"] {
|
|
width: 100%; padding: 9px 12px; border: 1.5px solid var(--border);
|
|
border-radius: 8px; font-size: 14px; color: var(--text-primary); background: var(--bg-input);
|
|
transition: border-color 0.15s;
|
|
}
|
|
.field input:focus { outline: none; border-color: #3B82F6; }
|
|
|
|
.color-picker-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
.color-picker-row input[type="color"] { width: 40px; height: 36px; border: none; border-radius: 6px; cursor: pointer; padding: 0; }
|
|
.color-hex { font-size: 13px; color: var(--text-muted); font-family: monospace; }
|
|
.color-presets { display: grid; grid-template-columns: repeat(10, 1fr); gap: 5px; }
|
|
.preset { aspect-ratio: 1; border-radius: 5px; cursor: pointer; transition: transform 0.1s; border: 2.5px solid transparent; box-sizing: border-box; }
|
|
.preset:hover { transform: scale(1.15); }
|
|
.preset.selected { border-color: var(--text-primary); transform: scale(1.1); }
|
|
|
|
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
|
|
</style>
|