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"
|
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"
|
active-class="bg-bg-soft text-green font-medium"
|
||||||
>
|
>
|
||||||
<span class="text-base leading-none">{{ l.icon }}</span>
|
<span :style="`font-size: var(--ui-menu-icon-size, 18px); line-height: 1`">{{ l.icon }}</span>
|
||||||
<span>{{ l.label }}</span>
|
<span :style="`font-size: var(--ui-menu-font-size, 13px)`">{{ l.label }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="px-4 py-4 border-t border-bg-soft text-text-muted text-xs">
|
<div class="px-4 py-4 border-t border-bg-soft text-text-muted text-xs">
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- 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 />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,11 +123,22 @@ function startDebugPolling() {
|
|||||||
}, 10000)
|
}, 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() {
|
async function loadDebugModeFromApi() {
|
||||||
try {
|
try {
|
||||||
const data = await settingsApi.get()
|
const data = await settingsApi.get()
|
||||||
debugMode.value = toBool(data.debug_mode)
|
debugMode.value = toBool(data.debug_mode)
|
||||||
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
|
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
|
||||||
|
applyUiSizesFromSettings(data)
|
||||||
} catch {
|
} catch {
|
||||||
// On garde la valeur locale.
|
// On garde la valeur locale.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,38 @@
|
|||||||
<div class="p-4 max-w-3xl mx-auto">
|
<div class="p-4 max-w-3xl mx-auto">
|
||||||
<h1 class="text-2xl font-bold text-green mb-4">Réglages</h1>
|
<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">
|
<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>
|
<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>
|
<p class="text-text-muted text-sm mb-3">Options globales de l'application.</p>
|
||||||
@@ -35,14 +67,49 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</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">
|
<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>
|
<h2 class="text-text font-semibold mb-2">Sauvegarde des données</h2>
|
||||||
<ul class="text-text-muted text-sm space-y-1">
|
<p class="text-text-muted text-sm mb-3">
|
||||||
<li>• Sauvegarde/restauration JSON de la base métier</li>
|
Exporte un ZIP téléchargeable contenant la base SQLite, les images/vidéos uploadées et les fichiers texte utiles.
|
||||||
<li>• Rotation/nettoyage des médias anciens</li>
|
</p>
|
||||||
<li>• Choix des unités météo (°C, mm, km/h)</li>
|
<div class="flex items-center gap-2">
|
||||||
<li>• Paramètres de seuils alertes (gel, pluie, vent)</li>
|
<button
|
||||||
</ul>
|
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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,6 +123,90 @@ const debugMode = ref(false)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const savedMsg = ref('')
|
const savedMsg = ref('')
|
||||||
const refreshingMeteo = ref(false)
|
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 {
|
function toBool(value: unknown): boolean {
|
||||||
if (typeof value === 'boolean') return value
|
if (typeof value === 'boolean') return value
|
||||||
@@ -73,6 +224,11 @@ async function loadSettings() {
|
|||||||
const data = await settingsApi.get()
|
const data = await settingsApi.get()
|
||||||
debugMode.value = toBool(data.debug_mode)
|
debugMode.value = toBool(data.debug_mode)
|
||||||
notifyDebugChanged(debugMode.value)
|
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 {
|
} catch {
|
||||||
// Laisse la valeur locale si l'API n'est pas disponible.
|
// 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(() => {
|
onMounted(() => {
|
||||||
void loadSettings()
|
void loadSettings()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user