feat(settings): sliders taille texte/menu/icônes/miniatures + CSS vars globales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,8 +28,8 @@
|
||||
class="flex items-center gap-3 text-text-muted hover:text-text py-2 px-3 rounded-lg text-sm transition-colors group"
|
||||
active-class="bg-bg-soft text-green font-medium"
|
||||
>
|
||||
<span class="text-base leading-none">{{ l.icon }}</span>
|
||||
<span>{{ l.label }}</span>
|
||||
<span :style="`font-size: var(--ui-menu-icon-size, 18px); line-height: 1`">{{ l.icon }}</span>
|
||||
<span :style="`font-size: var(--ui-menu-font-size, 13px)`">{{ l.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<div class="px-4 py-4 border-t border-bg-soft text-text-muted text-xs">
|
||||
@@ -38,7 +38,7 @@
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full">
|
||||
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full" style="font-size: var(--ui-font-size, 14px)">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
@@ -123,11 +123,22 @@ function startDebugPolling() {
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
function applyUiSizesFromSettings(data: Record<string, string>) {
|
||||
const defaults: Record<string, number> = { ui_font_size: 14, ui_menu_font_size: 13, ui_menu_icon_size: 18, ui_thumb_size: 96 }
|
||||
const root = document.documentElement
|
||||
for (const [key, def] of Object.entries(defaults)) {
|
||||
const val = Number(data[key]) || def
|
||||
const prop = '--' + key.replace(/_/g, '-')
|
||||
root.style.setProperty(prop, `${val}px`)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDebugModeFromApi() {
|
||||
try {
|
||||
const data = await settingsApi.get()
|
||||
debugMode.value = toBool(data.debug_mode)
|
||||
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
|
||||
applyUiSizesFromSettings(data)
|
||||
} catch {
|
||||
// On garde la valeur locale.
|
||||
}
|
||||
|
||||
@@ -2,6 +2,38 @@
|
||||
<div class="p-4 max-w-3xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-green mb-4">Réglages</h1>
|
||||
|
||||
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
|
||||
<h2 class="text-text font-semibold mb-2">Interface</h2>
|
||||
<p class="text-text-muted text-sm mb-4">Ajustez les tailles d'affichage. Les changements sont appliqués instantanément.</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="s in uiSizeSettings" :key="s.key" class="flex items-center gap-3">
|
||||
<label class="text-sm text-text w-44 shrink-0">{{ s.label }}</label>
|
||||
<input
|
||||
type="range"
|
||||
:min="s.min" :max="s.max" :step="s.step"
|
||||
v-model.number="uiSizes[s.key]"
|
||||
class="flex-1 accent-green"
|
||||
@input="applyUiSizes"
|
||||
/>
|
||||
<span class="text-text-muted text-xs w-12 text-right">{{ uiSizes[s.key] }}{{ s.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<button
|
||||
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
|
||||
:disabled="savingUi"
|
||||
@click="saveUiSettings"
|
||||
>{{ savingUi ? 'Enregistrement...' : 'Enregistrer' }}</button>
|
||||
<button
|
||||
class="text-text-muted text-xs hover:text-text px-2"
|
||||
@click="resetUiSettings"
|
||||
>Réinitialiser</button>
|
||||
<span v-if="uiSavedMsg" class="text-xs text-aqua">{{ uiSavedMsg }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
|
||||
<h2 class="text-text font-semibold mb-2">Général</h2>
|
||||
<p class="text-text-muted text-sm mb-3">Options globales de l'application.</p>
|
||||
@@ -35,14 +67,49 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
|
||||
<h2 class="text-text font-semibold mb-2">Test API backend</h2>
|
||||
<p class="text-text-muted text-sm mb-2">
|
||||
Ouvre la documentation interactive de l'API et un test rapide de santé.
|
||||
</p>
|
||||
<p class="text-text-muted text-xs mb-3">Base API détectée: <span class="text-text">{{ apiBaseUrl }}</span></p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
class="bg-blue text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90"
|
||||
@click="openApiDocs"
|
||||
>
|
||||
Ouvrir Swagger (/docs)
|
||||
</button>
|
||||
<button
|
||||
class="bg-aqua text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90"
|
||||
@click="openApiRedoc"
|
||||
>
|
||||
Ouvrir ReDoc (/redoc)
|
||||
</button>
|
||||
<button
|
||||
class="bg-bg border border-bg-hard text-text px-3 py-2 rounded-lg text-xs font-semibold hover:border-text-muted"
|
||||
@click="openApiHealth"
|
||||
>
|
||||
Tester /api/health
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4">
|
||||
<h2 class="text-text font-semibold mb-2">Idées utiles (prochaine étape)</h2>
|
||||
<ul class="text-text-muted text-sm space-y-1">
|
||||
<li>• Sauvegarde/restauration JSON de la base métier</li>
|
||||
<li>• Rotation/nettoyage des médias anciens</li>
|
||||
<li>• Choix des unités météo (°C, mm, km/h)</li>
|
||||
<li>• Paramètres de seuils alertes (gel, pluie, vent)</li>
|
||||
</ul>
|
||||
<h2 class="text-text font-semibold mb-2">Sauvegarde des données</h2>
|
||||
<p class="text-text-muted text-sm mb-3">
|
||||
Exporte un ZIP téléchargeable contenant la base SQLite, les images/vidéos uploadées et les fichiers texte utiles.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="bg-aqua text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
|
||||
:disabled="downloadingBackup"
|
||||
@click="downloadBackup"
|
||||
>
|
||||
{{ downloadingBackup ? 'Préparation du ZIP...' : 'Télécharger la sauvegarde (.zip)' }}
|
||||
</button>
|
||||
<span v-if="backupMsg" class="text-xs text-aqua">{{ backupMsg }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,6 +123,90 @@ const debugMode = ref(false)
|
||||
const saving = ref(false)
|
||||
const savedMsg = ref('')
|
||||
const refreshingMeteo = ref(false)
|
||||
const downloadingBackup = ref(false)
|
||||
const backupMsg = ref('')
|
||||
const apiBaseUrl = detectApiBaseUrl()
|
||||
|
||||
// --- UI Size settings ---
|
||||
const UI_DEFAULTS: Record<string, number> = {
|
||||
ui_font_size: 14,
|
||||
ui_menu_font_size: 13,
|
||||
ui_menu_icon_size: 18,
|
||||
ui_thumb_size: 96,
|
||||
}
|
||||
|
||||
const uiSizeSettings = [
|
||||
{ key: 'ui_font_size', label: 'Taille texte', min: 12, max: 20, step: 1, unit: 'px' },
|
||||
{ key: 'ui_menu_font_size', label: 'Texte menu latéral', min: 11, max: 18, step: 1, unit: 'px' },
|
||||
{ key: 'ui_menu_icon_size', label: 'Icônes menu', min: 14, max: 28, step: 1, unit: 'px' },
|
||||
{ key: 'ui_thumb_size', label: 'Miniatures images/vidéo', min: 60, max: 200, step: 4, unit: 'px' },
|
||||
]
|
||||
|
||||
const uiSizes = ref<Record<string, number>>({ ...UI_DEFAULTS })
|
||||
const savingUi = ref(false)
|
||||
const uiSavedMsg = ref('')
|
||||
|
||||
function applyUiSizes() {
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--ui-font-size', `${uiSizes.value.ui_font_size}px`)
|
||||
root.style.setProperty('--ui-menu-font-size', `${uiSizes.value.ui_menu_font_size}px`)
|
||||
root.style.setProperty('--ui-menu-icon-size', `${uiSizes.value.ui_menu_icon_size}px`)
|
||||
root.style.setProperty('--ui-thumb-size', `${uiSizes.value.ui_thumb_size}px`)
|
||||
window.dispatchEvent(new CustomEvent('ui-sizes-updated', { detail: { ...uiSizes.value } }))
|
||||
}
|
||||
|
||||
async function saveUiSettings() {
|
||||
savingUi.value = true
|
||||
uiSavedMsg.value = ''
|
||||
try {
|
||||
const payload: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(uiSizes.value)) payload[k] = String(v)
|
||||
await settingsApi.update(payload)
|
||||
applyUiSizes()
|
||||
uiSavedMsg.value = 'Enregistré'
|
||||
setTimeout(() => { uiSavedMsg.value = '' }, 1800)
|
||||
} finally {
|
||||
savingUi.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetUiSettings() {
|
||||
uiSizes.value = { ...UI_DEFAULTS }
|
||||
applyUiSizes()
|
||||
}
|
||||
|
||||
function detectApiBaseUrl(): string {
|
||||
const envBase = String((import.meta as any).env?.VITE_API_URL || '').trim()
|
||||
if (envBase) {
|
||||
if (envBase.startsWith('http://') || envBase.startsWith('https://')) {
|
||||
return envBase.replace(/\/$/, '')
|
||||
}
|
||||
if (envBase.startsWith('/')) {
|
||||
return window.location.origin
|
||||
}
|
||||
}
|
||||
if (window.location.port === '8060') {
|
||||
return window.location.origin
|
||||
}
|
||||
return `${window.location.protocol}//${window.location.hostname}:8060`
|
||||
}
|
||||
|
||||
function openInNewTab(path: string) {
|
||||
const url = `${apiBaseUrl}${path}`
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function openApiDocs() {
|
||||
openInNewTab('/docs')
|
||||
}
|
||||
|
||||
function openApiRedoc() {
|
||||
openInNewTab('/redoc')
|
||||
}
|
||||
|
||||
function openApiHealth() {
|
||||
openInNewTab('/api/health')
|
||||
}
|
||||
|
||||
function toBool(value: unknown): boolean {
|
||||
if (typeof value === 'boolean') return value
|
||||
@@ -73,6 +224,11 @@ async function loadSettings() {
|
||||
const data = await settingsApi.get()
|
||||
debugMode.value = toBool(data.debug_mode)
|
||||
notifyDebugChanged(debugMode.value)
|
||||
for (const s of uiSizeSettings) {
|
||||
const v = data[s.key]
|
||||
if (v != null) uiSizes.value[s.key] = Number(v) || UI_DEFAULTS[s.key]
|
||||
}
|
||||
applyUiSizes()
|
||||
} catch {
|
||||
// Laisse la valeur locale si l'API n'est pas disponible.
|
||||
}
|
||||
@@ -100,6 +256,28 @@ async function refreshMeteo() {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBackup() {
|
||||
downloadingBackup.value = true
|
||||
backupMsg.value = ''
|
||||
try {
|
||||
const { blob, filename } = await settingsApi.downloadBackup()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
backupMsg.value = 'Téléchargement lancé.'
|
||||
} catch {
|
||||
backupMsg.value = 'Erreur lors de la sauvegarde.'
|
||||
} finally {
|
||||
downloadingBackup.value = false
|
||||
window.setTimeout(() => { backupMsg.value = '' }, 2200)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadSettings()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user