Compare commits

...

4 Commits

Author SHA1 Message Date
DESKTOP-FUDKNA8\znjsz
c314f4b78e update datapage 2024-01-26 16:48:51 +08:00
DESKTOP-FUDKNA8\znjsz
086be4da66 fix lunbo 2024-01-26 16:38:23 +08:00
DESKTOP-FUDKNA8\znjsz
259752291c fix fullscreen 2024-01-26 16:35:51 +08:00
DESKTOP-FUDKNA8\znjsz
b6b17d1ef0 1 2024-01-26 16:01:56 +08:00
16 changed files with 729 additions and 90 deletions

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted } from "vue";
import MainPage from "./MainPage.vue";
import Slider from "./components/Slider.vue";
// import Slider from "./components/Slider.vue";
import useWebsocket from "./utils/useWebsocket";
import { useWsStore } from "./store";
const store = useWsStore();
@ -40,8 +40,10 @@ useWebsocket(store, urlPath.value);
<style scoped>
#app-container {
width: 100vw;
height: 100vh;
/* width: 100vw;
height: 100vh; */
background: #fff;
height: 100%;
display: inline-block;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, watch } from "vue";
import { ref, onMounted } from "vue";
import Tools from "./components/Tools.vue";
import NavMenu from "./components/NavMenu.vue";
import AnnoucementPage from "./pages/AnnouncementPage.vue";
@ -8,40 +8,57 @@ import AppHeader from "./components/Base/Header.vue";
import AlertListPage from "./pages/AlertListPage.vue";
import DataPage from "./pages/DataPage.vue";
import DatetimeTool from "./components/HeadTime.vue";
import TriplePage from "./pages/3D.vue";
import ThreeDimension from "./pages/ThreeDimension.vue";
import { useSettings } from "./store/settings";
const props = defineProps(["path"]);
const pages = ['3d', 'data', 'realtime', 'alert']
const pages = ['3d', 'data', 'realtime', 'alert', 'announcement']
const currentPage = ref("3d");
const handlePageChange = (page) => {
currentPage.value = page;
};
const mainContainer = ref(null);
const { settings } = useSettings();
const store = useSettings();
const timer = ref(null);
watch(() => settings.carousel, val => {
if (val) {
timer.value = setInterval(() => {
handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
}, settings.carouselTime * 1000);
return;
}
clearInterval(timer.value);
timer.value = null;
})
watch(() => settings.carouselTime, val => {
if (val > 0) {
store.$subscribe((mutation, state) => {
//
if (mutation.events.key == 'carouselTime' && state.settings.carouselTime > 0 && state.settings.carousel) {
if (timer.value) clearInterval(timer.value);
timer.value = setInterval(() => {
handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
}, val * 1000);
}, state.settings.carouselTime * 1000);
} else if (mutation.events.key == 'carousel') {
//
if (state.settings.carousel) {
timer.value = setInterval(() => {
handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
}, state.settings.carouselTime * 1000);
} else {
clearInterval(timer.value);
timer.value = null;
}
}
})
//
onMounted(() => {
const settings = store.settings;
if (settings.carousel) {
//
if (timer.value) clearInterval(timer.value);
timer.value = setInterval(() => {
handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
}, settings.carouselTime * 1000);
}
//
handleResolutionChange(settings.resolution.width, settings.resolution.height);
})
const pathMap = {
// 线
'/3-1': 1,
@ -61,9 +78,11 @@ const pathMap = {
}
function handleResolutionChange(width, height) {
console.log('document.documentElement', document.documentElement)
if (mainContainer.value) {
// mainContainer.value.style.width = `${width}px`;
// mainContainer.value.style.height = `${height}px`;
// changeScale(mainContainer.value, width, height)
changeScale(mainContainer.value, width, height)
}
}
@ -95,7 +114,8 @@ function resetScale(elm) {
@home="() => handlePageChange('3d')" />
<div v-else class="pages-wrapper">
<NavMenu @change="handlePageChange" :value="currentPage" />
<TriplePage v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" />
<!-- <TriplePage v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" /> -->
<ThreeDimension v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" />
<DataPage v-if="currentPage === 'data'" :line="pathMap[path] ?? '1'" />
<AlertListPage v-if="currentPage === 'alert'" :line="pathMap[path] ?? '1'" />
<RealtimePage v-if="currentPage === 'realtime'" :line="pathMap[path] ?? '1'" />
@ -111,10 +131,6 @@ function resetScale(elm) {
position: relative;
display: flex;
flex-direction: column;
/**
background: url('https://img1.baidu.com/it/u=2052683582,2603151390&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800')
100% / 100% no-repeat;
*/
background: url(./assets/bg.png) 100% / cover no-repeat;
}

View File

@ -29,7 +29,8 @@ onMounted(() => {
}
:fullscreen .datetime {
right: 500px;
/* right: 620px;
top: 56px; */
color: red;
}
</style>

View File

@ -62,9 +62,9 @@ const handleClick = (page) => {
z-index: 10;
}
:fullscreen .nav-menu {
/* :fullscreen .nav-menu {
top: 25%;
}
} */
.flex-list {
display: flex;

View File

@ -8,25 +8,25 @@ import { useSettings } from '../store/settings';
const emit = defineEmits(['change-resolution'])
const store = useSettings();
const settings = ref(store.settings)
store.$subscribe((_, state) => {
settings.value.carousel = state.settings.carousel;
})
const { settings, updateSettings } = useSettings();
// store.$subscribe((_, state) => {
// settings.value.carousel = state.settings.carousel;
// })
const visible = ref(false);
function toHome() {
// document.location.href = '/1-1'
document.location.reload()
}
function showDialog() {
visible.value = true;
}
function toggleLunbo() {
settings.value.carousel = !settings.value.carousel;
updateSettings({ type: 'carousel', value: null })
}
function handleChangeResolution(w, h) {
emit('change-resolution', w, h)
emit('change-resolution', w, h);
visible.value = false;
}
</script>
@ -40,7 +40,7 @@ function handleChangeResolution(w, h) {
</button>
<button style="position: relative;" @click="toggleLunbo">
<span v-if="settings.carousel"
style="color: #fffa; font-family: 'Courier New', Courier, monospace; font-weight:600; font-size: 22px; user-select: none; position: absolute; line-height: 52px; text-align: center;">
style="color: #fffa; font-family: 'Courier New', Courier, monospace; font-weight:600; width: 56px; font-size: 22px; user-select: none; position: absolute; line-height: 52px; text-align: center;">
{{ settings.carouselTime }}S</span>
<IconExchange :color="settings.carousel ? '#03356a' : '#50A1EC'" />
</button>

View File

@ -0,0 +1,116 @@
<script setup>
import { ref, watch, onMounted, nextTick } from "vue";
import * as echarts from "echarts";
import Container from "../Base/Container.vue";
import { useWsStore } from "../../store";
import chartSetup from "./HourChartOptions";
const store = useWsStore();
const chartChart = ref(null);
const chart = ref(null);
//
const hourData = ref([
// { lineName: '001', hour: '00:00', num: 10 },
// { lineName: '002', hour: '00:20', num: 20 },
// { lineName: '003', hour: '00:30', num: 30 },
// { lineName: '004', hour: '00:40', num: 14 },
// { lineName: '005', hour: '00:50', num: 50 }
]);
store.$subscribe((mutation, state) => {
// console.log("[HourChart] =======>", state.data2.lineHourList?.length);
if (
state.data2.lineHourList == undefined ||
state.data2.lineHourList?.length == 0
) {
// console.log("[HourChart] ");
hourData.value.splice(0);
if (chart.value) chart.value.dispose();
return;
}
// console.log("[HourChart] ===> : ", state.data2.lineHourList);
hourData.value = (state.data2?.lineHourList ?? [
// { lineName: '001', hour: '00:00', num: 10 },
// { lineName: '002', hour: '00:20', num: 20 },
// { lineName: '003', hour: '00:30', num: 30 },
// { lineName: '004', hour: '00:40', num: 14 },
// { lineName: '005', hour: '00:50', num: 50 },
]).map((item, index) => ({
id: `${item.lineName}_${index}`,
hour: item.hour || `${index}`.padStart(2, "0"),
data: item.num || Math.floor(Math.random() * 100),
}));
setupChart();
});
function setupChart() {
if (chart.value) chart.value.dispose();
nextTick(() => {
chart.value = echarts.init(chartChart.value);
// console.log("[HourChart] ===> chart: ", chart.value, hourData.value.map((item) => item.hour), hourData.value.map((item) => item.data));
chartSetup(
chart.value,
hourData.value.map((item) => item.hour),
hourData.value.map((item) => item.data)
);
});
// chart.value.setOption({
// xAxis: {
// type: "category",
// data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
// },
// yAxis: {
// type: "value",
// },
// series: [
// {
// data: [150, 230, 224, 218, 135, 147, 260],
// type: "line",
// },
// ],
// });
}
onMounted(() => {
chartChart.value.classList.add("h-full");
});
</script>
<template>
<Container class="chart" title="小时数据" icon="cube">
<div ref="chartChart" class="chart-chart" style="{opacity: (hourData && hourData.length > 0) ? 1 : 0}"></div>
<p v-show="!hourData || hourData.length === 0" class="empty-data-hint">
暂无数据
</p>
</Container>
</template>
<style scoped>
.chart {
/* height: 300px; */
}
.chart-chart {
height: 100%;
}
</style>
<style>
.empty-data-hint {
color: #c5c5c5;
letter-spacing: 1px;
font-size: 24px;
line-height: 1.25;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
display: inline-block;
width: 200px;
height: 32px;
user-select: none;
}
</style>

View File

@ -0,0 +1,63 @@
export const options = {
grid: {
top: 10,
bottom: 0,
left: 12,
right: 10,
containLabel: true,
},
xAxis: {
type: "category",
data: [],
axisLabel: {
fontSize: 16,
color: '#fff8'
},
axisLine: {
show: true,
lineStyle: {
color: "#e6e6e633",
},
},
},
yAxis: {
type: "value",
axisLine: {
show: true,
lineStyle: {
color: "#e6e6e633",
},
},
splitLine: {
lineStyle: {
color: '#e6e6e633'
}
},
axisLabel: {
fontSize: 16,
color: '#fff8'
},
minInterval: 1,
max: 100,
min: 1
},
series: [
{
data: [],
// data: Array.from({ length: 7 }, () => Math.random() * 100),
type: "bar",
// barWidth: 20,
showBackground: true,
backgroundStyle: {
color: "rgba(180, 180, 180, 0.2)",
},
},
],
};
export default function setup(echartInstance, timeArr, dataArr) {
const new_options = { ...options };
new_options.xAxis.data = timeArr;
new_options.series[0].data = dataArr;
echartInstance.setOption(new_options);
}

View File

@ -0,0 +1,74 @@
<script setup>
import { ref, onMounted, nextTick } from "vue";
import * as echarts from "echarts";
import Container from "../Base/Container.vue";
import { useWsStore } from "../../store";
import setupFn from "./TeamChartDayOptions";
const store = useWsStore();
const chartChart = ref(null);
const chart = ref(null);
const dayData = ref([]);
store.$subscribe((mutation, state) => {
console.log("[ChartDay] ===> state: ", state.data2.classTodayProductYield);
if (
state.data2.classTodayProductYield == undefined ||
state.data2.classTodayProductYield?.length == 0
) {
console.log("[ChartDay] ===> 清除状态");
dayData.value.splice(0);
if (chart.value) chart.value.dispose();
return;
}
for (let i = 0; i < state.data2.classTodayProductYield.length; ++i) {
if (state.data2.classTodayProductYield[i].teamName == "A组") {
dayData.value[0] = parseInt(state.data2.classTodayProductYield[i].yield);
} else if (state.data2.classTodayProductYield[i].teamName == "B组") {
dayData.value[1] = parseInt(state.data2.classTodayProductYield[i].yield);
} else if (state.data2.classTodayProductYield[i].teamName == "C组") {
dayData.value[2] = parseInt(state.data2.classTodayProductYield[i].yield);
}
}
console.log("[ChartDay] ===> dayData: ", dayData.value);
setupChart();
});
// onMounted(() => {
// nextTick(() => {
// setupChart();
// })
// })
function setupChart() {
if (chart.value) chart.value.dispose();
nextTick(() => {
chart.value = echarts.init(chartChart.value);
setupFn(chart.value, dayData.value);
});
}
onMounted(() => {
chartChart.value.classList.add("h-full");
});
</script>
<template>
<Container class="chart" title="本日班组情况" icon="cube">
<div ref="chartChart" class="chart-chart" style="{opacity: (dayData && dayData.length > 0) ? 1 : 0}"></div>
<p v-show="!dayData || dayData.length === 0" class="empty-data-hint">
暂无数据
</p>
</Container>
</template>
<style scoped>
.chart {
/* height: 300px; */
}
.chart-chart {
height: 100%;
}
</style>

View File

@ -0,0 +1,68 @@
export const options = {
color: ['#ffd601', '#72340b'],
grid: {
top: 10,
bottom: 0,
left: 0,
right: 28,
containLabel: true,
},
xAxis: {
max: 100,
splitLine: {
lineStyle: {
color: "#fff2",
},
},
axisLabel: {
fontSize: 16,
color: "#e5e5e5a3",
},
},
yAxis: {
type: "category",
data: ["A组", "B组", "C组"],
inverse: true,
animationDuration: 300,
animationDurationUpdate: 300,
max: 2, // only the largest 3 bars will be displayed
axisLabel: {
fontSize: 16,
color: "#e5e5e5a3",
},
splitLine: {
lineStyle: {
color: "#e5e5e5",
},
},
},
series: [
{
realtimeSort: true,
type: "bar",
// data: [10, 20, 30],
data: [],
label: {
show: true,
position: "right",
valueAnimation: true,
formatter: "{c}%",
color: "#fff",
fontSize: 16,
},
},
],
legend: {
show: false,
},
animationDuration: 0,
animationDurationUpdate: 3000,
animationEasing: "linear",
animationEasingUpdate: "linear",
};
export default function setup(echartInstance, dataArr) {
const new_options = { ...options };
new_options.series[0].data = dataArr;
echartInstance.setOption(new_options);
}

View File

@ -0,0 +1,73 @@
<script setup>
import { ref, onMounted, nextTick } from "vue";
import * as echarts from "echarts";
import Container from "../Base/Container.vue";
import { useWsStore } from "../../store";
import setupFn from "./TeamChartMonthOptions";
const store = useWsStore();
const chartChart = ref(null);
const chart = ref(null);
const monthData = ref(null);
store.$subscribe((mutation, state) => {
console.log("[ChartMonth] ===> state: ", state.data2.monthlyTarget);
if (
state.data2.monthlyTarget == undefined ||
state.data2.monthlyTarget?.length == 0
) {
console.log("[ChartMonth] ===> 清除状态");
monthData.value = null;
if (chart.value) chart.value.dispose();
return;
}
if (!state.data2.monthlyTarget[0]) return;
const { targetProduction, nowProduction, targetYield, nowYield } =
state.data2.monthlyTarget[0];
monthData.value = { targetProduction, nowProduction, targetYield, nowYield };
setupChart();
});
// 绿24FF5E
// FFB524
// FF3737
function setupChart() {
if (chart.value) chart.value.dispose();
nextTick(() => {
console.log("[ChartMonth] ===> 初始化表格: ", monthData.value);
chart.value = echarts.init(chartChart.value);
setupFn(chart.value, monthData.value);
});
}
onMounted(() => {
chartChart.value.classList.add("h-full");
// nextTick(() => {
// setupChart();
// })
});
</script>
<template>
<Container class="chart" title="本月班组情况" icon="cube">
<div ref="chartChart" class="chart-chart" style="{opacity: (monthData) ? 1 : 0}"></div>
<p v-show="!monthData" class="empty-data-hint">暂无数据</p>
</Container>
</template>
<style scoped>
.chart {
/* height: 300px; */
}
.chart-inner {}
.chart-chart {
height: 100%;
}
</style>

View File

@ -0,0 +1,176 @@
export const options = {
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0
},
title: [
{
text: "当前产量:" + 100 + " 片",
left: "27%",
textAlign: "center",
top: "70%",
textStyle: {
fontSize: 16,
color: '#fffa'
},
},
{
text: "目标产量:" + 100 + " 片",
left: "27%",
textAlign: "center",
top: "85%",
textStyle: {
fontSize: 16,
color: '#fffa'
},
},
{
text: "当前成品率:" + 22 + "%",
left: "72%",
textAlign: "center",
top: "70%",
textStyle: {
fontSize: 16,
color: '#fffa'
},
},
{
text: "目标成品率:" + 22 + "%",
left: "72%",
textAlign: "center",
top: "85%",
textStyle: {
fontSize: 16,
color: '#fffa'
},
},
],
series: [
{
type: "gauge",
startAngle: 90,
center: ["27%", "35%"],
endAngle: -270,
radius: "55%",
progress: {
show: true,
width: 12,
},
axisLine: {
lineStyle: {
width: 12,
},
},
pointer: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
detail: {
valueAnimation: true,
fontSize: 16,
offsetCenter: [0, "0%"],
formatter: "{value}%",
color: "rgba(255, 255, 255, 1)",
},
data: [
{
// value: (nowProduction / targetProduction * 100).toFixed(1),
value: 100,
},
],
},
{
type: "gauge",
startAngle: 90,
center: ["73%", "35%"],
endAngle: -270,
radius: "55%",
progress: {
show: true,
width: 12,
},
axisLine: {
lineStyle: {
width: 12,
},
},
pointer: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
detail: {
show: true,
valueAnimation: true,
fontSize: 16,
offsetCenter: [0, "0%"],
// formatter: 0 + "%",
// formatter: (nowYield / targetYield * 100).toFixed(1) + '%',
color: "#fff",
},
data: [
{
// value: targetYield,
value: 100,
name: "Perfect",
title: {
show: false,
},
detail: {
show: false,
valueAnimation: true,
offsetCenter: ["0%", "-20%"],
},
},
{
value: 100,
// value: nowYield,
name: "Good",
title: {
show: false,
},
detail: {
show: false,
valueAnimation: true,
offsetCenter: ["0%", "10%"],
},
},
{
value: 0,
detail: {
show: true,
},
},
],
},
],
};
export default function setup(echartInstance, data) {
const new_options = { ...options };
new_options.title[0].text = "当前产量:" + data.nowProduction + " 片";
new_options.title[1].text = "目标产量:" + data.targetProduction + " 片";
new_options.title[2].text = "当前成品率:" + data.nowYield + "%";
new_options.title[3].text = "目标成品率:" + data.targetYield + "%";
new_options.series[0].data[0].value = (data.nowProduction / data.targetProduction * 100).toFixed(1)
new_options.series[1].data[0].value = data.targetYield
new_options.series[1].data[1].value = data.nowYield
new_options.series[1].detail.formatter = (data.nowYield / data.targetYield * 100).toFixed(2) + '%',
echartInstance.setOption(new_options);
}

View File

@ -53,23 +53,23 @@ function changeScale(elm, width, height) {
elm.style.transform = style.transform;
elm.style.transformOrigin = style.transformOrigin;
}
onMounted(() => {
if (document.fullscreenElement && part3d.value) {
changeScale(part3d.value, document.documentElement.clientWidth, document.documentElement.clientHeight)
}
})
watch(() => settings.fullscreen, val => {
if (val) {
//
if (part3d.value) {
changeScale(part3d.value, document.documentElement.clientWidth, document.documentElement.clientHeight)
}
} else {
if (part3d.value) {
changeScale(part3d.value, 1920, 900)
}
}
}, { immediate: true })
// onMounted(() => {
// if (document.fullscreenElement && part3d.value) {
// changeScale(part3d.value, document.documentElement.clientWidth, document.documentElement.clientHeight)
// }
// })
// watch(() => settings.fullscreen, val => {
// if (val) {
// //
// if (part3d.value) {
// changeScale(part3d.value, document.documentElement.clientWidth, document.documentElement.clientHeight)
// }
// } else {
// if (part3d.value) {
// changeScale(part3d.value, 1920, 900)
// }
// }
// }, { immediate: true })
</script>
<template>
@ -629,9 +629,9 @@ watch(() => settings.fullscreen, val => {
transform: translateX(-58%);
}
:fullscreen .absolute-full {
/* :fullscreen .absolute-full {
transform: translateX(-80%);
}
} */
.info-1 .info:nth-child(1) {
/* left: 490px;

View File

@ -1,29 +1,20 @@
<!-- 实时数据页面 -->
<script setup>
import HourChart from "../components/HourChart.vue";
import TeamChartDay from "../components/TeamChartDay.vue";
import TeamChartMonth from "../components/TeamChartMonth.vue";
import ThreeD from './3D.vue'
import HourChart from "../components/datapage/HourChart.vue";
import TeamChartDay from "../components/datapage/TeamChartDay.vue";
import TeamChartMonth from "../components/datapage/TeamChartMonth.vue";
const props = defineProps({
line: {
type: Number,
default: 1,
},
});
</script>
<template>
<div class="data-page">
<div style="position: absolute; top: 0; left: -279px; width: calc(100% + 279px); height: 100%;">
<ThreeD :line="line ?? '1'" />
</div>
<div class="data-list">
<HourChart />
<TeamChartDay />
<TeamChartMonth />
</div>
<HourChart />
<TeamChartDay />
<TeamChartMonth />
<HourChart />
<TeamChartDay />
<TeamChartMonth />
</div>
</template>
@ -31,21 +22,16 @@ const props = defineProps({
.data-page {
flex: 1;
position: relative;
background: #cccc;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 24px;
padding: 32px;
align-items: stretch;
}
.data-list {
height: 100%;
width: 480px;
position: absolute;
top: 0;
right: 32px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
/*
:fullscreen .data-list {
width: 35%;
gap: 32px;
@ -55,5 +41,5 @@ const props = defineProps({
:fullscreen .data-list>div {
flex: 1;
}
} */
</style>

View File

@ -0,0 +1,59 @@
<!-- 实时数据页面 -->
<script setup>
import HourChart from "../components/HourChart.vue";
import TeamChartDay from "../components/TeamChartDay.vue";
import TeamChartMonth from "../components/TeamChartMonth.vue";
import ThreeD from './3D.vue'
const props = defineProps({
line: {
type: Number,
default: 1,
},
});
</script>
<template>
<div class="data-page">
<div style="position: absolute; top: 0; left: -279px; width: calc(100% + 279px); height: 100%;">
<ThreeD :line="line ?? '1'" />
</div>
<div class="data-list">
<HourChart />
<TeamChartDay />
<TeamChartMonth />
</div>
</div>
</template>
<style scoped>
.data-page {
flex: 1;
position: relative;
}
.data-list {
height: 100%;
width: 480px;
position: absolute;
top: 0;
right: 32px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
:fullscreen .data-list {
width: 35%;
gap: 32px;
padding: 32px 0;
justify-content: space-around;
}
:fullscreen .data-list>div {
flex: 1;
}
</style>

View File

@ -27,7 +27,8 @@ export const useSettings = defineStore("settings", () => {
watch(
() => settings.value.fullscreen,
(val) => {
const mainContainer = document.getElementById("main-container");
// const mainContainer = document.getElementById("main-container");
const mainContainer = document.documentElement;
if (val) {
mainContainer.requestFullscreen().then(() => {
document.removeEventListener("fullscreenchange", clearFullscreenFlag);

View File

@ -3,3 +3,7 @@
margin: 0;
font-family: '黑体', Arial, Helvetica, sans-serif;
}
html, body {
height: 100%;
}