shopfloor dashboard: fit-to-viewport scaling for TV kiosks
The board was a fixed-pixel layout with an internal overflow-y scroll, so on a TV (no scrolling) content past the fold was unreachable. Wrap it in a fixed 1920-wide surface and scale it to fill the viewport (ResizeObserver + resize), so the whole board is visible edge-to-edge at any resolution. Drop the .dashboard-content max-height/overflow scroll.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shopfloor-dashboard">
|
<div class="shopfloor-dashboard">
|
||||||
|
<div class="dash-fit" ref="fitEl">
|
||||||
<header class="dashboard-header">
|
<header class="dashboard-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<img :src="siteLogo" alt="Site logo" class="logo" />
|
<img :src="siteLogo" alt="Site logo" class="logo" />
|
||||||
@@ -180,10 +181,11 @@
|
|||||||
Auto-refreshes every 30 seconds
|
Auto-refreshes every 30 seconds
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
|
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
|
||||||
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
|
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
|
||||||
import { withBase } from '@/utils/basePath'
|
import { withBase } from '@/utils/basePath'
|
||||||
@@ -204,6 +206,20 @@ let refreshInterval = null
|
|||||||
let recognitionInterval = null
|
let recognitionInterval = null
|
||||||
let recertPageInterval = null
|
let recertPageInterval = null
|
||||||
|
|
||||||
|
// Fit-to-viewport: the board is a fixed 1920-wide design; scale the whole thing
|
||||||
|
// to fill the screen so a TV shows everything with no scrolling, at any size.
|
||||||
|
const fitEl = ref(null)
|
||||||
|
let resizeObserver = null
|
||||||
|
function fitToScreen() {
|
||||||
|
const el = fitEl.value
|
||||||
|
if (!el) return
|
||||||
|
const w = el.offsetWidth || 1
|
||||||
|
const h = el.offsetHeight || 1
|
||||||
|
const scale = Math.min(window.innerWidth / w, window.innerHeight / h)
|
||||||
|
el.style.transform = `scale(${scale})`
|
||||||
|
el.style.left = `${Math.max(0, (window.innerWidth - w * scale) / 2)}px`
|
||||||
|
}
|
||||||
|
|
||||||
// The board groups cards by each type's configured display style, so any custom
|
// The board groups cards by each type's configured display style, so any custom
|
||||||
// type set to carousel/grid/banner renders that way - not just the built-ins.
|
// type set to carousel/grid/banner renders that way - not just the built-ins.
|
||||||
const SPECIAL_STYLES = ['carousel', 'grid', 'banner']
|
const SPECIAL_STYLES = ['carousel', 'grid', 'banner']
|
||||||
@@ -318,12 +334,22 @@ onMounted(async () => {
|
|||||||
currentRecertPage.value = (currentRecertPage.value + 1) % recertPageCount.value
|
currentRecertPage.value = (currentRecertPage.value + 1) % recertPageCount.value
|
||||||
}
|
}
|
||||||
}, 7000)
|
}, 7000)
|
||||||
|
|
||||||
|
// Scale to fit once rendered, and re-fit whenever the content or window
|
||||||
|
// changes size (new data, banner cycling, resolution change).
|
||||||
|
await nextTick()
|
||||||
|
fitToScreen()
|
||||||
|
resizeObserver = new ResizeObserver(() => fitToScreen())
|
||||||
|
if (fitEl.value) resizeObserver.observe(fitEl.value)
|
||||||
|
window.addEventListener('resize', fitToScreen)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (refreshInterval) clearInterval(refreshInterval)
|
if (refreshInterval) clearInterval(refreshInterval)
|
||||||
if (recognitionInterval) clearInterval(recognitionInterval)
|
if (recognitionInterval) clearInterval(recognitionInterval)
|
||||||
if (recertPageInterval) clearInterval(recertPageInterval)
|
if (recertPageInterval) clearInterval(recertPageInterval)
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
window.removeEventListener('resize', fitToScreen)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
@@ -417,13 +443,25 @@ function handlePhotoError(e) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shopfloor-dashboard {
|
.shopfloor-dashboard {
|
||||||
min-height: 100vh;
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
background: #00003d;
|
background: #00003d;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixed 1920-wide design surface; JS scales it to fill the viewport (TV) so
|
||||||
|
the whole board is visible with no scrolling. */
|
||||||
|
.dash-fit {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1920px;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
@@ -485,8 +523,6 @@ function handlePhotoError(e) {
|
|||||||
|
|
||||||
.dashboard-content {
|
.dashboard-content {
|
||||||
padding: 20px 40px;
|
padding: 20px 40px;
|
||||||
max-height: calc(100vh - 180px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
|||||||
Reference in New Issue
Block a user