<template>
|
<div ref="rootRef" class="green-screen" :class="{ 'is-fullscreen': isFullscreen }">
|
<header class="green-screen__header">
|
<div class="brand">
|
<div class="brand__mark" />
|
<div>
|
<div class="brand__title">绿色校园运营驾驶舱</div>
|
<div class="brand__sub">{{ current?.audience }}视角 · {{ current?.label }}</div>
|
</div>
|
</div>
|
|
<nav class="nav">
|
<button
|
v-for="item in SCREEN_NAV"
|
:key="item.key"
|
class="nav__item"
|
:class="{ 'is-active': item.key === currentKey }"
|
type="button"
|
@click="go(item.key)"
|
>
|
<span class="nav__index">{{ item.index }}</span>
|
<span>{{ item.label }}</span>
|
</button>
|
</nav>
|
|
<div class="tools">
|
<cl-select-button v-model="timeRange" :options="TIME_RANGE_OPTIONS" small />
|
<el-button :icon="Refresh" circle :loading="loading" @click="reload" />
|
<el-button :icon="isFullscreen ? Close : FullScreen" circle @click="toggleFullscreen" />
|
</div>
|
</header>
|
|
<section v-loading="loading" class="green-screen__body">
|
<slot />
|
</section>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
import { Close, FullScreen, Refresh } from '@element-plus/icons-vue';
|
import {
|
SCREEN_HOME_PATH,
|
SCREEN_NAV,
|
TIME_RANGE_OPTIONS,
|
resolveScreenKey,
|
type ScreenKey
|
} from '../data/screen';
|
import { provideScreenTime, useScreenData, useScreenFullscreen } from '../hooks/use-screen';
|
|
const route = useRoute();
|
const router = useRouter();
|
const timeRange = provideScreenTime();
|
const { loading, reload } = useScreenData();
|
const { isFullscreen, setFullscreen } = useScreenFullscreen();
|
const rootRef = ref<HTMLElement>();
|
|
const currentKey = computed<ScreenKey>(() =>
|
resolveScreenKey(route.params.page as string, route.query.page as string)
|
);
|
|
const current = computed(() => SCREEN_NAV.find(e => e.key === currentKey.value));
|
|
/** 始终留在首页路径,用 ?page= 切换。默认页是地图,overview 也必须带 page,否则会被解析回 map */
|
function go(key: ScreenKey) {
|
if (key === currentKey.value) return;
|
const query = { ...route.query } as Record<string, any>;
|
query.page = key;
|
router.replace({ path: SCREEN_HOME_PATH, query });
|
}
|
|
function syncFullscreen() {
|
setFullscreen(document.fullscreenElement === rootRef.value);
|
// 全屏后触发布局重算,让 echarts / 子页 flex 高度铺满
|
window.setTimeout(() => {
|
window.dispatchEvent(new Event('resize'));
|
}, 320);
|
}
|
|
async function toggleFullscreen() {
|
const el = rootRef.value;
|
if (!el) return;
|
|
if (document.fullscreenElement === el) {
|
await document.exitFullscreen();
|
} else if (document.fullscreenElement) {
|
await document.exitFullscreen();
|
await el.requestFullscreen();
|
} else {
|
await el.requestFullscreen();
|
}
|
}
|
|
onMounted(() => {
|
document.addEventListener('fullscreenchange', syncFullscreen);
|
});
|
|
onUnmounted(() => {
|
document.removeEventListener('fullscreenchange', syncFullscreen);
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.green-screen {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
min-height: calc(100vh - 118px);
|
background: linear-gradient(180deg, var(--el-bg-color-page) 0%, var(--el-fill-color-lighter) 100%);
|
color: var(--el-text-color-primary);
|
|
&.is-fullscreen {
|
padding: 12px;
|
height: 100vh;
|
min-height: 100vh;
|
background: #07111f;
|
|
.green-screen__header,
|
.brand__title {
|
color: #e5e7eb;
|
}
|
|
.brand__sub {
|
color: #94a3b8;
|
}
|
|
.green-screen__body {
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
|
:deep(> *) {
|
flex: 1;
|
min-height: 0;
|
height: 100%;
|
}
|
|
/* 02/03/04 等子页在全屏下纵向铺满 */
|
:deep(.screen-page) {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
|
.kpi-row,
|
.kpi-grid {
|
flex-shrink: 0;
|
margin-bottom: 0 !important;
|
}
|
|
.mid-row,
|
.el-row:not(.kpi-row) {
|
flex: 1;
|
min-height: 0;
|
margin-bottom: 0 !important;
|
display: flex !important;
|
|
> .el-col {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
}
|
}
|
|
.panel-card {
|
flex: 1;
|
min-height: 0 !important;
|
height: 100%;
|
}
|
|
.panel-card__body {
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
min-height: 0;
|
}
|
|
.chart,
|
.chart--bar,
|
.chart--value,
|
.chart--rank,
|
.flow,
|
.note {
|
flex: 1;
|
min-height: 0;
|
height: 100% !important;
|
}
|
|
/* 全屏时强制刷新 echarts 容器高度 */
|
.echarts,
|
[_echarts_instance_] {
|
height: 100% !important;
|
width: 100% !important;
|
}
|
}
|
}
|
}
|
|
&__header {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
padding: 8px 4px 12px;
|
flex-shrink: 0;
|
}
|
|
&__body {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
padding-bottom: 8px;
|
}
|
}
|
|
.brand {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
min-width: 220px;
|
|
&__mark {
|
width: 12px;
|
height: 36px;
|
border-radius: 6px;
|
background: linear-gradient(180deg, #22c55e, #16a34a);
|
}
|
|
&__title {
|
font-size: 18px;
|
font-weight: 700;
|
letter-spacing: 0.5px;
|
line-height: 1.2;
|
}
|
|
&__sub {
|
margin-top: 2px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
}
|
}
|
|
.nav {
|
display: flex;
|
flex: 1;
|
gap: 8px;
|
overflow-x: auto;
|
|
&__item {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
height: 36px;
|
padding: 0 12px;
|
border: 1px solid var(--el-border-color-extra-light);
|
border-radius: 999px;
|
background: var(--el-bg-color);
|
color: var(--el-text-color-regular);
|
white-space: nowrap;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
|
&:hover,
|
&.is-active {
|
border-color: var(--el-color-primary-light-5);
|
color: var(--el-color-primary);
|
background: var(--el-color-primary-light-9);
|
}
|
}
|
|
&__index {
|
font-size: 11px;
|
font-weight: 700;
|
opacity: 0.7;
|
}
|
}
|
|
.tools {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
flex-shrink: 0;
|
}
|
|
@media (max-width: 992px) {
|
.green-screen__header {
|
flex-wrap: wrap;
|
align-items: flex-start;
|
gap: 8px;
|
}
|
|
.brand {
|
min-width: 0;
|
flex: 1 1 auto;
|
}
|
|
.nav {
|
order: 3;
|
flex: 1 1 100%;
|
}
|
|
.tools {
|
max-width: 100%;
|
overflow-x: auto;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.green-screen {
|
min-height: 0;
|
height: 100%;
|
}
|
|
.brand__title {
|
font-size: 16px;
|
}
|
}
|
</style>
|