From 8954db4cf4969166961c72569dfa24645276b947 Mon Sep 17 00:00:00 2001
From: wangrong <wangrong@shsening.com>
Date: 星期日, 30 八月 2026 11:48:15 +0800
Subject: [PATCH] 修复首页问题

---
 src/modules/report/views/screen/overview.vue            |   19 
 src/modules/report/components/screen-camera-preview.vue |   16 
 src/modules/report/views/screen/carbon.vue              |   21 -
 src/modules/monitor/views/cameras.vue                   |  117 ++++++-
 src/modules/report/components/screen-auto-scroll.vue    |  142 ++++++++++
 src/modules/report/views/screen/participation.vue       |   21 -
 src/modules/report/views/screen/index.vue               |   17 
 src/modules/report/views/screen/resource.vue            |   21 -
 src/modules/report/styles/screen-kpi.scss               |   19 +
 src/modules/report/data/screen.ts                       |   22 +
 src/modules/report/components/kpi-card.vue              |   61 ++-
 src/modules/report/views/screen/operation.vue           |  154 +++++-----
 src/modules/report/hooks/use-screen.ts                  |   51 +++
 src/modules/report/components/screen-shell.vue          |  156 +++++++---
 14 files changed, 576 insertions(+), 261 deletions(-)

diff --git a/src/modules/monitor/views/cameras.vue b/src/modules/monitor/views/cameras.vue
index 19fe750..94372c4 100644
--- a/src/modules/monitor/views/cameras.vue
+++ b/src/modules/monitor/views/cameras.vue
@@ -1,17 +1,17 @@
 <template>
   <div class="camera-page">
     <div class="scroll-wrapper">
-      <el-button class="arrow left" @click="scrollLeft" :disabled="canScrollLeft" circle>
+      <el-button class="arrow left" @click="scrollLeftManual" :disabled="canScrollLeft" circle>
         <el-icon><ArrowLeft /></el-icon>
       </el-button>
 
-      <div class="scroll-viewport" ref="viewportRef">
+      <div class="scroll-viewport" ref="viewportRef" @mouseenter="pauseAuto" @mouseleave="resumeAuto">
         <div class="scroll-track" :style="{ transform: `translateX(${-scrollPos}px)` }">
           <div v-for="dev in deviceList" :key="dev.id" class="player-card">
             <ezuikit-player
               v-if="ysToken"
-              :key="'p_' + dev.id + '_' + ysToken"
-              :container-id="'player_' + dev.id"
+              :key="'p_' + dev.id + '_' + ysToken + '_' + playerRemountKey"
+              :container-id="'player_' + dev.id + '_' + playerRemountKey"
               :access-token="ysToken"
               :url="`ezopen://open.ys7.com/${dev.iotCode || dev.code}/1.hd.live`"
               :pause-after-first-frame="true"
@@ -20,7 +20,7 @@
         </div>
       </div>
 
-      <el-button class="arrow right" @click="scrollRight" :disabled="canScrollRight" circle>
+      <el-button class="arrow right" @click="scrollRightManual" :disabled="canScrollRight" circle>
         <el-icon><ArrowRight /></el-icon>
       </el-button>
     </div>
@@ -44,42 +44,100 @@
 const loading = ref(false);
 const scrollPos = ref(0);
 const viewportRef = ref<HTMLElement | null>(null);
+/** 鍏ㄥ睆鍒囨崲鍚庨�掑锛屽己鍒堕噸寤鸿悿鐭虫挱鏀惧櫒 */
+const playerRemountKey = ref(0);
 
-// 姣忔婊氬姩鐨勮窛绂伙紙涓�涓憚鍍忓ご鐨勫搴� + 闂磋窛锛�
+function onScreenFullscreenChange() {
+  playerRemountKey.value += 1;
+}
+
+// 鑷姩婊氬姩鎺у埗
+const AUTO_INTERVAL = 3000; // 姣忔婊氬姩闂撮殧(ms)
+const autoPlay = ref(true); // 鏄惁鑷姩婊氬姩
+let autoTimer: ReturnType<typeof setInterval> | null = null;
+
+// 姣忔婊氬姩鐨勮窛绂�
 const scrollStep = computed(() => {
-  const cardWidth = 312; // 300px 瀹藉害 + 12px 闂磋窛鐨勪竴鍗�
+  const cardWidth = 300; // 涓� player-card 瀹藉害涓�鑷�
   const gap = 12;
   return cardWidth + gap;
 });
 
-// 鏄惁鍙互鍚戝乏婊氬姩
-const canScrollLeft = computed(() => scrollPos.value <= 0);
-
-// 鏄惁鍙互鍚戝彸婊氬姩
-const canScrollRight = computed(() => {
-  if (!viewportRef.value) return true;
+// 鍙粴鍔ㄧ殑鏈�澶ц窛绂�
+const maxScroll = computed(() => {
+  if (!viewportRef.value) return 0;
   const viewportWidth = viewportRef.value.clientWidth;
   const trackWidth = deviceList.value.length * scrollStep.value;
-  // 澧炲姞 2px 瀹瑰樊
-  return scrollPos.value >= trackWidth - viewportWidth - 2;
+  return Math.max(0, trackWidth - viewportWidth);
 });
+
+// 鏄惁鍙互鍚戝乏/鍚戝彸婊氬姩锛堟墜鍔ㄧ澶寸敤锛�
+const canScrollLeft = computed(() => scrollPos.value <= 0);
+const canScrollRight = computed(() => scrollPos.value >= maxScroll.value - 2);
+
+// 鑷姩婊氬姩鏂瑰悜: 1 鍚戝彸, -1 鍚戝乏
+const direction = ref(1);
 
 function scrollLeft() {
   scrollPos.value = Math.max(0, scrollPos.value - scrollStep.value);
 }
 
 function scrollRight() {
-  if (!viewportRef.value) return;
-  const viewportWidth = viewportRef.value.clientWidth;
-  const trackWidth = deviceList.value.length * scrollStep.value;
-  const maxScroll = trackWidth - viewportWidth;
-  
-  // 濡傛灉鍓╀綑璺濈灏忎簬涓�涓闀匡紝鐩存帴婊氬姩鍒板簳閮�
-  if (scrollPos.value + scrollStep.value >= maxScroll) {
-    scrollPos.value = maxScroll;
+  const max = maxScroll.value;
+  if (scrollPos.value + scrollStep.value >= max) {
+    scrollPos.value = max;
   } else {
     scrollPos.value += scrollStep.value;
   }
+}
+
+// 鎵ц涓�姝ヨ嚜鍔ㄦ粴鍔紙鍒拌竟鐣岃嚜鍔ㄦ崲鍚戯紝鍥炲埌璧风偣寰幆锛�
+function autoTick() {
+  if (deviceList.value.length === 0) return;
+  if (maxScroll.value <= 0) return; // 涓�灞忚涓嬪叏閮紝涓嶆粴鍔�
+
+  scrollPos.value += scrollStep.value * direction.value;
+
+  if (scrollPos.value >= maxScroll.value) {
+    scrollPos.value = maxScroll.value;
+    direction.value = -1; // 鍒版渶鍙筹紝鍥炲ご鍚戝乏
+  } else if (scrollPos.value <= 0) {
+    scrollPos.value = 0;
+    direction.value = 1; // 鍥炲埌璧风偣锛屽惊鐜悜鍙�
+    pauseAuto();          // 鍥炲埌璧风偣鍋滀竴涓嬶紙鍙�夛紝瑙佷笅鏂� startAuto锛�
+    startAuto();          // 绔嬪嵆寮�濮嬫柊涓�杞�
+  }
+}
+
+function startAuto() {
+  stopAuto();
+  if (!autoPlay.value) return;
+  autoTimer = setInterval(autoTick, AUTO_INTERVAL);
+}
+
+function stopAuto() {
+  if (autoTimer) {
+    clearInterval(autoTimer);
+    autoTimer = null;
+  }
+}
+
+// 榧犳爣鎮仠鏆傚仠锛岀Щ寮�缁х画
+function pauseAuto() {
+  stopAuto();
+}
+function resumeAuto() {
+  startAuto();
+}
+
+// 鎵嬪姩鐐瑰嚮绠ご锛氶噸缃嚜鍔ㄦ粴鍔ㄥ畾鏃跺櫒锛岄伩鍏嶄笌鑷姩鑺傚鍐茬獊
+function scrollLeftManual() {
+  scrollLeft();
+  startAuto();
+}
+function scrollRightManual() {
+  scrollRight();
+  startAuto();
 }
 
 async function fetchYsToken() {
@@ -98,6 +156,9 @@
     const res = await service.basicdata.iot.getCameras();
     deviceList.value = (res || []) as any[];
     scrollPos.value = 0;
+    direction.value = 1;
+    // 鏁版嵁鍔犺浇瀹屽啀鍚姩鑷姩婊氬姩
+    startAuto();
   } catch (e: any) {
     console.error('鑾峰彇鎽勫儚澶村垪琛ㄥけ璐�', e?.response?.data?.message || e.message);
     deviceList.value = [];
@@ -107,22 +168,26 @@
 }
 
 async function refreshDevices() {
+  stopAuto();
   await fetchDevices();
 }
 
 function handleKeydown(e: KeyboardEvent) {
-  if (e.key === 'ArrowLeft') scrollLeft();
-  if (e.key === 'ArrowRight') scrollRight();
+  if (e.key === 'ArrowLeft') { scrollLeftManual(); }
+  if (e.key === 'ArrowRight') { scrollRightManual(); }
 }
 
 onMounted(async () => {
   window.addEventListener('keydown', handleKeydown);
+  window.addEventListener('report-screen-fullscreen-change', onScreenFullscreenChange);
   await fetchYsToken();
-  await fetchDevices();
+  await fetchDevices(); // 鍐呴儴浼氬湪鏁版嵁灏辩华鍚� startAuto
 });
 
 onUnmounted(() => {
+  stopAuto();
   window.removeEventListener('keydown', handleKeydown);
+  window.removeEventListener('report-screen-fullscreen-change', onScreenFullscreenChange);
 });
 </script>
 
diff --git a/src/modules/report/components/kpi-card.vue b/src/modules/report/components/kpi-card.vue
index ed1c0ce..151ebe0 100644
--- a/src/modules/report/components/kpi-card.vue
+++ b/src/modules/report/components/kpi-card.vue
@@ -1,13 +1,10 @@
 <template>
-	<div class="kpi-card" :class="{ 'is-alert': alert, 'is-demo': demo }">
-		<div class="kpi-card__label">
-			<span>{{ label }}</span>
-			<el-tag v-if="demo" class="kpi-card__badge" type="info" effect="plain" size="small">婕旂ず</el-tag>
-		</div>
+	<div class="kpi-card" :class="{ 'is-alert': alert }">
+		<div class="kpi-card__label" :title="label">{{ label }}</div>
 		<div class="kpi-card__value">
 			<cl-number :value="numeric" :fixed="fixed" type="amount" :suffix="suffix" />
 		</div>
-		<div v-if="hint" class="kpi-card__hint">{{ hint }}</div>
+		<div class="kpi-card__hint" :title="hint || ''">{{ hint || '\u00A0' }}</div>
 	</div>
 </template>
 
@@ -21,6 +18,7 @@
 	fixed?: number;
 	hint?: string;
 	alert?: boolean;
+	/** @deprecated 棣栭〉涓嶅啀灞曠ず婕旂ず鏍囪瘑锛屼繚鐣� prop 浠ュ厤璋冪敤鏂规姤閿� */
 	demo?: boolean;
 }>();
 
@@ -29,31 +27,47 @@
 
 <style lang="scss" scoped>
 .kpi-card {
-	padding: 14px 16px;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	width: 100%;
+	height: 112px;
+	padding: 12px 14px;
 	border-radius: 12px;
 	border: 1px solid var(--el-border-color-extra-light);
 	background: var(--el-bg-color);
 	color: var(--el-text-color-primary);
-	min-height: 96px;
+	overflow: hidden;
 
 	&__label {
-		display: flex;
-		align-items: center;
-		gap: 6px;
+		flex-shrink: 0;
+		height: 20px;
+		line-height: 20px;
 		font-size: 13px;
 		color: var(--el-text-color-secondary);
-	}
-
-	&__badge {
-		transform: scale(0.92);
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
 	}
 
 	&__value {
-		margin-top: 8px;
-		font-size: 26px;
+		flex: 1;
+		display: flex;
+		align-items: center;
+		min-height: 0;
+		margin: 4px 0;
+		font-size: 24px;
 		font-weight: 700;
-		line-height: 1.2;
+		line-height: 1.15;
 		color: var(--el-text-color-primary);
+		overflow: hidden;
+
+		:deep(.cl-number) {
+			width: 100%;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
 
 		:deep(.cl-number__suffix) {
 			margin-left: 4px;
@@ -64,9 +78,14 @@
 	}
 
 	&__hint {
-		margin-top: 8px;
+		flex-shrink: 0;
+		height: 18px;
+		line-height: 18px;
 		font-size: 12px;
 		color: var(--el-text-color-secondary);
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
 	}
 
 	&.is-alert {
@@ -76,10 +95,6 @@
 		.kpi-card__value {
 			color: var(--el-color-danger);
 		}
-	}
-
-	&.is-demo:not(.is-alert) {
-		border-style: dashed;
 	}
 }
 </style>
diff --git a/src/modules/report/components/screen-auto-scroll.vue b/src/modules/report/components/screen-auto-scroll.vue
new file mode 100644
index 0000000..0f7d562
--- /dev/null
+++ b/src/modules/report/components/screen-auto-scroll.vue
@@ -0,0 +1,142 @@
+<template>
+	<div class="auto-scroll" @mouseenter="pause" @mouseleave="resume">
+		<div v-if="$slots.head" class="auto-scroll__head">
+			<slot name="head" />
+		</div>
+		<div ref="viewportRef" class="auto-scroll__viewport">
+			<div ref="contentRef" class="auto-scroll__content">
+				<slot />
+			</div>
+		</div>
+	</div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({
+	name: 'report-screen-auto-scroll'
+});
+
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+
+const props = withDefaults(
+	defineProps<{
+		/** 婊氬姩姝ヨ繘鍍忕礌 */
+		step?: number;
+		/** 婊氬姩闂撮殧 ms */
+		interval?: number;
+		/** 渚濊禆鍙樺寲鏃堕噸鏂版娴嬫槸鍚﹂渶瑕佹粴鍔紙濡傚垪琛ㄦ暟鎹級 */
+		watchKey?: string | number;
+	}>(),
+	{
+		step: 1,
+		interval: 40
+	}
+);
+
+const viewportRef = ref<HTMLElement | null>(null);
+const contentRef = ref<HTMLElement | null>(null);
+const paused = ref(false);
+const direction = ref(1);
+let timer: ReturnType<typeof setInterval> | null = null;
+let resizeObserver: ResizeObserver | null = null;
+
+function canScroll() {
+	const el = viewportRef.value;
+	if (!el) return false;
+	return el.scrollHeight > el.clientHeight + 2;
+}
+
+function tick() {
+	const el = viewportRef.value;
+	if (!el || paused.value || !canScroll()) return;
+
+	const max = el.scrollHeight - el.clientHeight;
+	let next = el.scrollTop + props.step * direction.value;
+
+	if (next >= max) {
+		next = max;
+		direction.value = -1;
+	} else if (next <= 0) {
+		next = 0;
+		direction.value = 1;
+	}
+
+	el.scrollTop = next;
+}
+
+function start() {
+	stop();
+	timer = setInterval(tick, props.interval);
+}
+
+function stop() {
+	if (timer) {
+		clearInterval(timer);
+		timer = null;
+	}
+}
+
+function pause() {
+	paused.value = true;
+}
+
+function resume() {
+	paused.value = false;
+}
+
+async function reset() {
+	await nextTick();
+	const el = viewportRef.value;
+	if (el) el.scrollTop = 0;
+	direction.value = 1;
+}
+
+onMounted(() => {
+	start();
+	const el = viewportRef.value;
+	if (el && typeof ResizeObserver !== 'undefined') {
+		resizeObserver = new ResizeObserver(() => {
+			reset();
+		});
+		resizeObserver.observe(el);
+		if (contentRef.value) resizeObserver.observe(contentRef.value);
+	}
+	window.addEventListener('resize', reset);
+	window.addEventListener('report-screen-fullscreen-change', reset);
+});
+
+onUnmounted(() => {
+	stop();
+	resizeObserver?.disconnect();
+	window.removeEventListener('resize', reset);
+	window.removeEventListener('report-screen-fullscreen-change', reset);
+});
+
+watch(
+	() => props.watchKey,
+	() => reset()
+);
+</script>
+
+<style lang="scss" scoped>
+.auto-scroll {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	min-height: 0;
+
+	&__head {
+		flex-shrink: 0;
+	}
+
+	&__viewport {
+		flex: 1;
+		min-height: 0;
+		overflow: hidden;
+	}
+
+	&__content {
+		min-height: min-content;
+	}
+}
+</style>
diff --git a/src/modules/report/components/screen-camera-preview.vue b/src/modules/report/components/screen-camera-preview.vue
index 210bc90..41ac8fb 100644
--- a/src/modules/report/components/screen-camera-preview.vue
+++ b/src/modules/report/components/screen-camera-preview.vue
@@ -9,8 +9,8 @@
 				<div class="screen-cameras__player">
 					<ezuikit-player
 						v-if="ysToken"
-						:key="'screen_cam_' + dev.id + '_' + ysToken"
-						:container-id="'screen_cam_' + dev.id"
+						:key="'screen_cam_' + dev.id + '_' + ysToken + '_' + playerRemountKey"
+						:container-id="'screen_cam_' + dev.id + '_' + playerRemountKey"
 						:access-token="ysToken"
 						:url="cameraUrl(dev)"
 						:pause-after-first-frame="true"
@@ -27,7 +27,7 @@
 	name: 'report-screen-camera-preview'
 });
 
-import { computed, onMounted, ref } from 'vue';
+import { computed, onMounted, onUnmounted, ref } from 'vue';
 import { useCool } from '/@/cool';
 import EzuikitPlayer from '../../ezuikit/components/ezuikit-player.vue';
 
@@ -45,6 +45,11 @@
 const previewList = computed(() => deviceList.value.slice(0, maxCount.value));
 
 const { service } = useCool();
+const playerRemountKey = ref(0);
+
+function onScreenFullscreenChange() {
+	playerRemountKey.value += 1;
+}
 
 const deviceList = ref<any[]>([]);
 const ysToken = ref('');
@@ -78,9 +83,14 @@
 }
 
 onMounted(async () => {
+	window.addEventListener('report-screen-fullscreen-change', onScreenFullscreenChange);
 	await fetchYsToken();
 	await fetchDevices();
 });
+
+onUnmounted(() => {
+	window.removeEventListener('report-screen-fullscreen-change', onScreenFullscreenChange);
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/src/modules/report/components/screen-shell.vue b/src/modules/report/components/screen-shell.vue
index e990e92..23d0b88 100644
--- a/src/modules/report/components/screen-shell.vue
+++ b/src/modules/report/components/screen-shell.vue
@@ -4,7 +4,7 @@
 			<div class="brand">
 				<div class="brand__mark" />
 				<div>
-					<div class="brand__title">鏍″洯缁胯壊杩愯惀椹鹃┒鑸�</div>
+					<div class="brand__title">缁胯壊鏍″洯杩愯惀椹鹃┒鑸�</div>
 					<div class="brand__sub">{{ current?.audience }}瑙嗚 路 {{ current?.label }}</div>
 				</div>
 			</div>
@@ -16,7 +16,7 @@
 					class="nav__item"
 					:class="{ 'is-active': item.key === currentKey }"
 					type="button"
-					@click="go(item.path)"
+					@click="go(item.key)"
 				>
 					<span class="nav__index">{{ item.index }}</span>
 					<span>{{ item.label }}</span>
@@ -24,17 +24,6 @@
 			</nav>
 
 			<div class="tools">
-				<el-tag v-if="loadError" type="danger" effect="plain" size="small">{{ loadError }}</el-tag>
-				<el-tag v-if="dataMode === 'demo'" type="info" effect="plain" size="small">
-					婕旂ず鏁版嵁 路 寰呬笟鍔℃暟鎹帴鍏�
-				</el-tag>
-				<el-tag v-else-if="dataMode === 'mixed'" type="warning" effect="plain" size="small">
-					閮ㄥ垎婕旂ず 路 閮ㄥ垎鎸囨爣寰呰ˉ鏁�
-				</el-tag>
-				<el-tag v-else-if="unavailable.length" type="warning" effect="plain" size="small">
-					閮ㄥ垎鎸囨爣寰呰ˉ鏁�
-				</el-tag>
-				<el-tag v-else type="success" effect="plain" size="small">瀹炴椂鏁版嵁</el-tag>
 				<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" />
@@ -43,20 +32,6 @@
 
 		<section v-loading="loading" class="green-screen__body">
 			<slot />
-			<el-collapse v-if="dataMode === 'mixed' && demoSections.length" class="missing">
-				<el-collapse-item title="褰撳墠涓烘紨绀烘暟鎹殑鍖哄潡" name="demo">
-					<ul>
-						<li v-for="item in demoSections" :key="item.section">{{ item.label }}</li>
-					</ul>
-				</el-collapse-item>
-			</el-collapse>
-			<el-collapse v-if="unavailable.length" class="missing">
-				<el-collapse-item title="褰撳墠鎷夸笉鍒版垨鍙e緞寰呯‘璁ょ殑鎸囨爣" name="1">
-					<ul>
-						<li v-for="item in unavailable" :key="item">{{ item }}</li>
-					</ul>
-				</el-collapse-item>
-			</el-collapse>
 		</section>
 	</div>
 </template>
@@ -65,32 +40,46 @@
 import { computed, onMounted, onUnmounted, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { Close, FullScreen, Refresh } from '@element-plus/icons-vue';
-import { SCREEN_NAV, TIME_RANGE_OPTIONS, type ScreenKey } from '../data/screen';
-import { provideScreenTime, useScreenData, useScreenFieldSource } from '../hooks/use-screen';
+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 { data, dataMode, loading, loadError, reload } = useScreenData();
-const { demoSections } = useScreenFieldSource();
+const { loading, reload } = useScreenData();
+const { isFullscreen, setFullscreen } = useScreenFullscreen();
 const rootRef = ref<HTMLElement>();
-const isFullscreen = ref(false);
 
-const unavailable = computed(() => data.value.unavailable || []);
-
-const currentKey = computed<ScreenKey>(() => {
-	const page = String(route.params.page || 'overview') as ScreenKey;
-	return SCREEN_NAV.some(e => e.key === page) ? page : 'overview';
-});
+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));
 
-function go(path: string) {
-	if (path !== route.path) router.push(path);
+/** 濮嬬粓鐣欏湪棣栭〉璺緞锛岀敤 ?page= 鍒囨崲 pageMap锛岄伩鍏嶈烦鍒� /overview 涓㈡帀瀵艰埅澹� */
+function go(key: ScreenKey) {
+	if (key === currentKey.value) return;
+	const query = { ...route.query } as Record<string, any>;
+	if (key === 'overview') {
+		delete query.page;
+	} else {
+		query.page = key;
+	}
+	router.replace({ path: SCREEN_HOME_PATH, query });
 }
 
 function syncFullscreen() {
-	isFullscreen.value = document.fullscreenElement === rootRef.value;
+	setFullscreen(document.fullscreenElement === rootRef.value);
+	// 鍏ㄥ睆鍚庤Е鍙戝竷灞�閲嶇畻锛岃 echarts / 瀛愰〉 flex 楂樺害閾烘弧
+	window.setTimeout(() => {
+		window.dispatchEvent(new Event('resize'));
+	}, 320);
 }
 
 async function toggleFullscreen() {
@@ -127,6 +116,7 @@
 
 	&.is-fullscreen {
 		padding: 12px;
+		height: 100vh;
 		min-height: 100vh;
 		background: #07111f;
 
@@ -137,6 +127,77 @@
 
 		.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;
+				}
+			}
 		}
 	}
 
@@ -223,18 +284,5 @@
 	align-items: center;
 	gap: 10px;
 	flex-shrink: 0;
-}
-
-.missing {
-	margin: 8px 0 0;
-	border: none;
-
-	ul {
-		margin: 0;
-		padding-left: 18px;
-		line-height: 1.8;
-		color: var(--el-text-color-regular);
-		font-size: 13px;
-	}
 }
 </style>
diff --git a/src/modules/report/data/screen.ts b/src/modules/report/data/screen.ts
index 658c00a..a8b56f3 100644
--- a/src/modules/report/data/screen.ts
+++ b/src/modules/report/data/screen.ts
@@ -2,14 +2,24 @@
 export type ScreenKey = 'overview' | 'resource' | 'carbon' | 'participation' | 'operation';
 export type RankMetric = 'weight' | 'people' | 'pointPerCapita' | 'weightPerCapita' | 'carbon';
 
-export const SCREEN_NAV: { key: ScreenKey; path: string; index: string; label: string; audience: string }[] = [
-	{ key: 'overview', path: '/report/screen/overview', index: '01', label: '缁胯壊鏍″洯鎬昏', audience: '鏍¢瀵�' },
-	{ key: 'resource', path: '/report/screen/resource', index: '02', label: '鍨冨溇璧勬簮鍖栧垎鏋�', audience: '鍚庡嫟 / 鐜繚' },
-	{ key: 'carbon', path: '/report/screen/carbon', index: '03', label: '纰冲噺鎺掍笌缁胯壊璐$尞', audience: '鍙岀⒊ / ESG' },
-	{ key: 'participation', path: '/report/screen/participation', index: '04', label: '甯堢敓鍙備笌涓庣⒊绉垎', audience: '瀛﹀伐 / 鍥㈠' },
-	{ key: 'operation', path: '/report/screen/operation', index: '05', label: '鍥炴敹杩愯惀鐩戞帶', audience: '杩愯惀鍏徃' }
+/** 棣栭〉鑿滃崟鍥哄畾涓� /report/screen/index锛屽瓙椤甸�氳繃 ?page= 鍒囨崲锛岄伩鍏嶇寮�棣栭〉鍏ュ彛 */
+export const SCREEN_HOME_PATH = '/report/screen/index';
+
+export const SCREEN_NAV: { key: ScreenKey; index: string; label: string; audience: string }[] = [
+	{ key: 'overview', index: '01', label: '缁胯壊鏍″洯鎬昏', audience: '鏍¢瀵�' },
+	{ key: 'resource', index: '02', label: '鍨冨溇璧勬簮鍖栧垎鏋�', audience: '鍚庡嫟 / 鐜繚' },
+	{ key: 'carbon', index: '03', label: '纰冲噺鎺掍笌缁胯壊璐$尞', audience: '鍙岀⒊ / ESG' },
+	{ key: 'participation', index: '04', label: '甯堢敓鍙備笌涓庣⒊绉垎', audience: '瀛﹀伐 / 鍥㈠' },
+	{ key: 'operation', index: '05', label: '鍥炴敹杩愯惀鐩戞帶', audience: '杩愯惀鍏徃' }
 ];
 
+/** 浠庤矾鐢辫В鏋愬綋鍓嶅ぇ灞忓瓙椤碉紱index / 绌� / 闈炴硶鍊煎潎鍥炶惤鍒� overview */
+export function resolveScreenKey(page?: string | string[] | null, queryPage?: string | string[] | null): ScreenKey {
+	const raw = String(queryPage || page || 'overview');
+	if (raw === 'index') return 'overview';
+	return SCREEN_NAV.some(e => e.key === raw) ? (raw as ScreenKey) : 'overview';
+}
+
 export const TIME_RANGE_OPTIONS: { label: string; value: TimeRange }[] = [
 	{ label: '浠婃棩', value: 'today' },
 	{ label: '鏈懆', value: 'week' },
diff --git a/src/modules/report/hooks/use-screen.ts b/src/modules/report/hooks/use-screen.ts
index a6621c0..0b7dc34 100644
--- a/src/modules/report/hooks/use-screen.ts
+++ b/src/modules/report/hooks/use-screen.ts
@@ -3,9 +3,7 @@
 import { useTheme } from '/#/theme';
 import { useCool } from '/@/cool';
 import {
-	groupDemoFields,
-	isDemoField,
-	isDemoSection
+	groupDemoFields
 } from '../data/screen-field-meta';
 import {
 	initialScreenData,
@@ -22,6 +20,9 @@
 const liveFields = ref<string[]>(initial.liveFields);
 const loading = ref(false);
 const loadError = ref('');
+/** 椹鹃┒鑸辨槸鍚﹀浜庢祻瑙堝櫒鍏ㄥ睆锛涜悿鐭虫挱鏀惧櫒鍦ㄥ叏灞忓垏鎹㈠悗闇� remount 鎵嶈兘鎭㈠鐢婚潰 */
+const screenFullscreen = ref(false);
+const playerRemountKey = ref(0);
 
 export function provideScreenTime() {
 	return sharedTimeRange;
@@ -29,6 +30,34 @@
 
 export function useScreenTime() {
 	return sharedTimeRange;
+}
+
+export function useScreenFullscreen() {
+	function bumpPlayerRemount(delay = 280) {
+		window.setTimeout(() => {
+			playerRemountKey.value += 1;
+			window.dispatchEvent(
+				new CustomEvent('report-screen-fullscreen-change', {
+					detail: { remountKey: playerRemountKey.value }
+				})
+			);
+		}, delay);
+	}
+
+	function setFullscreen(active: boolean) {
+		const changed = screenFullscreen.value !== active;
+		screenFullscreen.value = active;
+		if (changed) {
+			bumpPlayerRemount();
+		}
+	}
+
+	return {
+		isFullscreen: screenFullscreen,
+		playerRemountKey,
+		bumpPlayerRemount,
+		setFullscreen
+	};
 }
 
 export function useScreenData() {
@@ -73,7 +102,11 @@
 			applyResolved(resolveScreenData(res, sharedTimeRange.value));
 		} catch (e: any) {
 			applyResolved(resolveScreenData(null, sharedTimeRange.value));
-			loadError.value = e?.message || '椹鹃┒鑸辨帴鍙h皟鐢ㄥけ璐�';
+			const msg = e?.message || '';
+			// 棣栭〉闈欓粯闄嶇骇鍒版紨绀烘暟鎹紝涓嶅睍绀虹櫥褰�/鏉冮檺绫婚敊璇枃妗�
+			if (!/鐧诲綍澶辨晥|鏃犳潈闄�/.test(msg)) {
+				loadError.value = msg || '椹鹃┒鑸辨帴鍙h皟鐢ㄥけ璐�';
+			}
 		} finally {
 			loading.value = false;
 		}
@@ -91,18 +124,18 @@
 	return { load, loading, loadError, dataMode };
 }
 
-/** 鍒ゆ柇鍖哄潡/瀛楁鏄惁涓烘紨绀烘暟鎹紙mixed 妯″紡涓嬫寜瀛楁绾у垽瀹氾級 */
+/** 棣栭〉涓嶅睍绀恒�屾紨绀恒�嶆爣璇嗭紝搴曞眰浠嶅彲闈欓粯浣跨敤婕旂ず鏁版嵁濉厖 */
 export function useScreenFieldSource() {
 	const { dataMode, demoFields } = useScreenData();
 
 	const demoSections = computed(() => groupDemoFields(demoFields.value));
 
-	function demoSection(section: string) {
-		return isDemoSection(section, demoFields.value, dataMode.value);
+	function demoSection(_section: string) {
+		return false;
 	}
 
-	function demoField(field: string) {
-		return isDemoField(field, demoFields.value, dataMode.value);
+	function demoField(_field: string) {
+		return false;
 	}
 
 	return {
diff --git a/src/modules/report/styles/screen-kpi.scss b/src/modules/report/styles/screen-kpi.scss
new file mode 100644
index 0000000..b123be7
--- /dev/null
+++ b/src/modules/report/styles/screen-kpi.scss
@@ -0,0 +1,19 @@
+/* 椹鹃┒鑸� KPI 琛岋細绛夊绛夐珮鍗$墖缃戞牸 */
+.kpi-grid {
+	display: grid;
+	gap: 10px;
+	margin-bottom: 10px;
+	grid-template-columns: repeat(var(--kpi-cols, 6), minmax(0, 1fr));
+
+	@media (max-width: 1400px) {
+		grid-template-columns: repeat(min(var(--kpi-cols, 6), 4), minmax(0, 1fr));
+	}
+
+	@media (max-width: 992px) {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	@media (max-width: 576px) {
+		grid-template-columns: 1fr;
+	}
+}
diff --git a/src/modules/report/views/screen/carbon.vue b/src/modules/report/views/screen/carbon.vue
index 13110a8..8c06102 100644
--- a/src/modules/report/views/screen/carbon.vue
+++ b/src/modules/report/views/screen/carbon.vue
@@ -1,10 +1,8 @@
 <template>
 	<div class="screen-page">
-			<el-row :gutter="10" class="kpi-row">
-				<el-col v-for="item in kpis" :key="item.label" :lg="4" :sm="12" :xs="24">
-					<kpi-card v-bind="item" :demo="isDemoField(item.field)" />
-				</el-col>
-			</el-row>
+		<div class="kpi-grid" style="--kpi-cols: 6">
+			<kpi-card v-for="item in kpis" :key="item.label" v-bind="item" />
+		</div>
 
 			<el-row :gutter="10" class="mid-row">
 				<el-col :lg="14" :xs="24">
@@ -50,7 +48,7 @@
 import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
 
 const { data, timeRange } = useScreenData();
-const { isDemoField, isDemoSection } = useScreenFieldSource();
+const { isDemoSection } = useScreenFieldSource();
 const { textColor, mutedColor, splitColor, primary, baseGrid, axisStyle, tooltip } = useChartTheme();
 
 const kpis = computed(() => [
@@ -148,19 +146,12 @@
 </script>
 
 <style lang="scss" scoped>
+@import '../../styles/screen-kpi.scss';
+
 .screen-page {
-	.kpi-row,
 	.mid-row {
 		margin-bottom: 10px;
-	}
 
-	.kpi-row {
-		:deep(.el-col) {
-			margin-bottom: 10px;
-		}
-	}
-
-	.mid-row {
 		:deep(.panel-card) {
 			min-height: 360px;
 		}
diff --git a/src/modules/report/views/screen/index.vue b/src/modules/report/views/screen/index.vue
index f56eea8..cd0b453 100644
--- a/src/modules/report/views/screen/index.vue
+++ b/src/modules/report/views/screen/index.vue
@@ -10,20 +10,19 @@
 });
 
 import { computed } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { useRoute } from 'vue-router';
 import ScreenShell from '../../components/screen-shell.vue';
 import Overview from './overview.vue';
 import Resource from './resource.vue';
 import Carbon from './carbon.vue';
 import Participation from './participation.vue';
 import Operation from './operation.vue';
-import { SCREEN_NAV, type ScreenKey } from '../../data/screen';
+import { resolveScreenKey, type ScreenKey } from '../../data/screen';
 import { useScreenLoader } from '../../hooks/use-screen';
 
 useScreenLoader();
 
 const route = useRoute();
-const router = useRouter();
 
 const pageMap = {
 	overview: Overview,
@@ -33,12 +32,8 @@
 	operation: Operation
 };
 
-const currentKey = computed<ScreenKey>(() => {
-	const page = String(route.params.page || 'overview') as ScreenKey;
-	return SCREEN_NAV.some(e => e.key === page) ? page : 'overview';
-});
-
-if (!route.params.page) {
-	router.replace('/report/screen/overview');
-}
+/** 鍋滅暀鍦� /report/screen/index锛岀敤 query.page锛堟垨鍔ㄦ�佽矾鐢� :page锛夊垏鎹㈠瓙椤碉紝涓嶅啀寮哄埗璺宠蛋 */
+const currentKey = computed<ScreenKey>(() =>
+	resolveScreenKey(route.params.page as string, route.query.page as string)
+);
 </script>
diff --git a/src/modules/report/views/screen/operation.vue b/src/modules/report/views/screen/operation.vue
index 42c3164..8356869 100644
--- a/src/modules/report/views/screen/operation.vue
+++ b/src/modules/report/views/screen/operation.vue
@@ -1,39 +1,35 @@
 <template>
 	<div class="screen-page">
-		<el-row :gutter="10" class="kpi-row">
-			<el-col :lg="3" :sm="8" :xs="12">
-				<kpi-card label="鍦ㄧ嚎鍒嗘嫞鍛�" :value="data.kpis.sorterOnline"
-					:hint="`${data.kpis.sorterOnline} / ${data.kpis.sorterTotal}`"
-					:demo="isDemoField('kpis.sorterOnline')" />
-			</el-col>
-			<el-col :lg="3" :sm="8" :xs="12">
-				<kpi-card label="鍦ㄧ嚎杞﹁締" :value="data.kpis.vehicleOnline"
-					:hint="`${data.kpis.vehicleOnline} / ${data.kpis.vehicleTotal}`"
-					:demo="isDemoField('kpis.vehicleOnline')" />
-			</el-col>
-			<el-col :lg="3" :sm="8" :xs="12">
-				<kpi-card label="鍦ㄧ嚎鍒嗘嫞鐐�" :value="data.kpis.siteOnline"
-					:hint="`${data.kpis.siteOnline} / ${data.kpis.siteTotal}锛屾寜绔欑偣鍚敤`"
-					:demo="isDemoField('kpis.siteOnline')" />
-			</el-col>
-			<el-col :lg="3" :sm="8" :xs="12">
-				<kpi-card label="浠婃棩鍥炴敹璁㈠崟" :value="data.kpis.todayOrders" :demo="isDemoField('kpis.todayOrders')" />
-			</el-col>
-			<el-col :lg="4" :sm="8" :xs="12">
-				<kpi-card label="浠婃棩棰勭害 / 寰呭鐞�" :value="data.kpis.todayAppointments"
-					:hint="`寰呭鐞� ${data.kpis.pendingAppointments}`"
-					:demo="isDemoField('kpis.todayAppointments')" />
-			</el-col>
-			<el-col :lg="4" :sm="8" :xs="12">
-				<kpi-card label="寮傚父鍛婅" :value="data.kpis.alerts" alert
-					:hint="`鍋ュ悍寮傚父 ${data.kpis.healthAlerts}锛屼笉灞曠ず闅愮浣撳緛`"
-					:demo="isDemoField('kpis.alerts')" />
-			</el-col>
-			<el-col :lg="4" :sm="8" :xs="12">
-				<kpi-card label="杞﹁締瀹夊叏浜嬩欢" :value="safetyCount" hint="瓒呴�� / 鎬ュ埞绛夛紝鏉ヨ嚜杞﹁締鏃ユ眹鎬�"
-					:demo="isDemoSection('vehicles')" />
-			</el-col>
-		</el-row>
+		<div class="kpi-grid" style="--kpi-cols: 7">
+			<kpi-card
+				label="鍦ㄧ嚎鍒嗘嫞鍛�"
+				:value="data.kpis.sorterOnline"
+				:hint="`${data.kpis.sorterOnline} / ${data.kpis.sorterTotal}`"
+			/>
+			<kpi-card
+				label="鍦ㄧ嚎杞﹁締"
+				:value="data.kpis.vehicleOnline"
+				:hint="`${data.kpis.vehicleOnline} / ${data.kpis.vehicleTotal}`"
+			/>
+			<kpi-card
+				label="鍦ㄧ嚎鍒嗘嫞鐐�"
+				:value="data.kpis.siteOnline"
+				:hint="`${data.kpis.siteOnline} / ${data.kpis.siteTotal}锛屾寜绔欑偣鍚敤`"
+			/>
+			<kpi-card label="浠婃棩鍥炴敹璁㈠崟" :value="data.kpis.todayOrders" />
+			<kpi-card
+				label="浠婃棩棰勭害 / 寰呭鐞�"
+				:value="data.kpis.todayAppointments"
+				:hint="`寰呭鐞� ${data.kpis.pendingAppointments}`"
+			/>
+			<kpi-card
+				label="寮傚父鍛婅"
+				:value="data.kpis.alerts"
+				alert
+				:hint="`鍋ュ悍寮傚父 ${data.kpis.healthAlerts}锛屼笉灞曠ず闅愮浣撳緛`"
+			/>
+			<kpi-card label="杞﹁締瀹夊叏浜嬩欢" :value="safetyCount" hint="瓒呴�� / 鎬ュ埞绛夛紝鏉ヨ嚜杞﹁締鏃ユ眹鎬�" />
+		</div>
 
 		<el-row :gutter="10" class="mid-row">
 			<el-col :lg="14" :xs="24">
@@ -58,7 +54,7 @@
 			</el-col>
 		</el-row>
 
-		<el-row :gutter="10">
+		<el-row :gutter="10" class="bottom-row">
 			<el-col :lg="8" :xs="24">
 				<panel-card title="鍒嗘嫞鍛樼姸鎬�" sub="鏉ヨ嚜鍒嗘嫞鍛樻棩琛紝鍙眹鎬荤姸鎬侊紝涓嶅睍绀轰綋寰佹槑缁�" :demo="isDemoSection('sorterStatus')">
 					<div class="status-list">
@@ -72,14 +68,16 @@
 			</el-col>
 			<el-col :lg="8" :xs="24">
 				<panel-card title="杞﹁締杩愯" sub="鍏徃棰嗗鐪嬭繍鍔涗笌瀹夊叏锛屼笉杩涙牎棰嗗棣栭〉" :demo="isDemoSection('vehicles')">
-					<div class="table table--vehicle">
-						<div class="table__head">
-							<span>杞︾墝</span>
-							<span>閲岀▼km</span>
-							<span>鎬犻�熷垎</span>
-							<span>瓒呴��</span>
-							<span>鐘舵��</span>
-						</div>
+					<screen-auto-scroll class="table table--vehicle" :watch-key="vehicleWatchKey">
+						<template #head>
+							<div class="table__head">
+								<span>杞︾墝</span>
+								<span>閲岀▼km</span>
+								<span>鎬犻�熷垎</span>
+								<span>瓒呴��</span>
+								<span>鐘舵��</span>
+							</div>
+						</template>
 						<div v-for="item in data.vehicles" :key="item.no" class="table__row">
 							<span>{{ item.no }}</span>
 							<span>{{ item.km }}</span>
@@ -87,23 +85,25 @@
 							<span>{{ item.overspeed }}</span>
 							<span :class="item.status === '鍦ㄧ嚎' ? 'is-on' : 'is-off'">{{ item.status }}</span>
 						</div>
-					</div>
+					</screen-auto-scroll>
 				</panel-card>
 			</el-col>
 			<el-col :lg="8" :xs="24">
 				<panel-card title="鍒嗘嫞鐐瑰悶鍚�" sub="杩愯惀缁村害鎵嶄笅閽诲埌鐐逛綅" :demo="isDemoSection('sites')">
-					<div class="table table--site">
-						<div class="table__head">
-							<span>鏀堕泦鐐�</span>
-							<span>浠婃棩kg</span>
-							<span>鍦ㄧ嚎</span>
-						</div>
+					<screen-auto-scroll class="table table--site" :watch-key="siteWatchKey">
+						<template #head>
+							<div class="table__head">
+								<span>鏀堕泦鐐�</span>
+								<span>浠婃棩kg</span>
+								<span>鍦ㄧ嚎</span>
+							</div>
+						</template>
 						<div v-for="item in data.sites" :key="item.name" class="table__row">
 							<span>{{ item.name }}</span>
 							<span>{{ item.weight }}</span>
 							<span :class="item.online ? 'is-on' : 'is-off'">{{ item.online ? '鍦ㄧ嚎' : '绂荤嚎' }}</span>
 						</div>
-					</div>
+					</screen-auto-scroll>
 				</panel-card>
 			</el-col>
 		</el-row>
@@ -118,13 +118,16 @@
 import { computed } from 'vue';
 import KpiCard from '../../components/kpi-card.vue';
 import PanelCard from '../../components/panel-card.vue';
+import ScreenAutoScroll from '../../components/screen-auto-scroll.vue';
 import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
 
 const { data } = useScreenData();
-const { isDemoField, isDemoSection } = useScreenFieldSource();
+const { isDemoSection } = useScreenFieldSource();
 const { mutedColor, splitColor, tooltip } = useChartTheme();
 
 const safetyCount = computed(() => data.value.vehicles.reduce((s, e) => s + e.overspeed, 0));
+const vehicleWatchKey = computed(() => data.value.vehicles.map(e => e.no).join('|'));
+const siteWatchKey = computed(() => data.value.sites.map(e => e.name).join('|'));
 
 function funnelWidth(value: number) {
 	const max = data.value.appointmentFlow[0]?.value || 1;
@@ -165,17 +168,11 @@
 </script>
 
 <style lang="scss" scoped>
-.screen-page {
+@import '../../styles/screen-kpi.scss';
 
-	.kpi-row,
+.screen-page {
 	.mid-row {
 		margin-bottom: 10px;
-	}
-
-	.kpi-row {
-		:deep(.el-col) {
-			margin-bottom: 10px;
-		}
 	}
 
 	.mid-row,
@@ -272,32 +269,47 @@
 		}
 	}
 
-	.table {
-		padding: 8px 12px 16px;
-		font-size: 13px;
+	.bottom-row {
+		:deep(.panel-card) {
+			min-height: 280px;
+		}
 
-		&--vehicle &__head,
-		&--vehicle &__row {
+		:deep(.panel-card__body) {
+			display: flex;
+			flex-direction: column;
+			min-height: 0;
+			overflow: hidden;
+		}
+	}
+
+	.table {
+		padding: 8px 12px 12px;
+		font-size: 13px;
+		height: 100%;
+		min-height: 0;
+
+		&--vehicle :deep(.table__head),
+		&--vehicle .table__row {
 			grid-template-columns: 1.4fr repeat(4, 0.8fr);
 		}
 
-		&--site &__head,
-		&--site &__row {
+		&--site :deep(.table__head),
+		&--site .table__row {
 			grid-template-columns: 1.6fr 0.8fr 0.6fr;
 		}
 
-		&__head,
-		&__row {
+		:deep(.table__head),
+		.table__row {
 			display: grid;
 			gap: 8px;
 			padding: 10px 4px;
 		}
 
-		&__head {
+		:deep(.table__head) {
 			color: var(--el-text-color-secondary);
 		}
 
-		&__row {
+		.table__row {
 			border-top: 1px solid var(--el-border-color-extra-light);
 		}
 
diff --git a/src/modules/report/views/screen/overview.vue b/src/modules/report/views/screen/overview.vue
index bab6c1d..5227902 100644
--- a/src/modules/report/views/screen/overview.vue
+++ b/src/modules/report/views/screen/overview.vue
@@ -1,10 +1,8 @@
 <template>
 	<div class="screen-page">
-		<el-row :gutter="10" class="kpi-row">
-			<el-col v-for="item in kpis" :key="item.label" :xl="3" :lg="6" :sm="12" :xs="24">
-				<kpi-card v-bind="item" :demo="isDemoField(item.field)" />
-			</el-col>
-		</el-row>
+		<div class="kpi-grid" style="--kpi-cols: 8">
+			<kpi-card v-for="item in kpis" :key="item.label" v-bind="item" />
+		</div>
 
 		<el-row :gutter="10" class="charts-row">
 			<el-col :lg="7" :xs="24">
@@ -56,7 +54,7 @@
 import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
 
 const { data, timeRange } = useScreenData();
-const { isDemoField, isDemoSection } = useScreenFieldSource();
+const { isDemoSection } = useScreenFieldSource();
 const { textColor, mutedColor, splitColor, primary, baseGrid, axisStyle, tooltip } = useChartTheme();
 
 const mixMetric = ref<'weight' | 'value' | 'count'>('weight');
@@ -171,17 +169,12 @@
 </script>
 
 <style lang="scss" scoped>
+@import '../../styles/screen-kpi.scss';
+
 .screen-page {
-	.kpi-row,
 	.charts-row,
 	.camera-row {
 		margin-bottom: 10px;
-	}
-
-	.kpi-row {
-		:deep(.el-col) {
-			margin-bottom: 10px;
-		}
 	}
 
 	.charts-row {
diff --git a/src/modules/report/views/screen/participation.vue b/src/modules/report/views/screen/participation.vue
index 4653c74..aeba5a7 100644
--- a/src/modules/report/views/screen/participation.vue
+++ b/src/modules/report/views/screen/participation.vue
@@ -1,10 +1,8 @@
 <template>
 	<div class="screen-page">
-			<el-row :gutter="10" class="kpi-row">
-				<el-col v-for="item in kpis" :key="item.label" :lg="4" :sm="12" :xs="24">
-					<kpi-card v-bind="item" :demo="isDemoField(item.field)" />
-				</el-col>
-			</el-row>
+		<div class="kpi-grid" style="--kpi-cols: 6">
+			<kpi-card v-for="item in kpis" :key="item.label" v-bind="item" />
+		</div>
 
 			<el-row :gutter="10" class="mid-row">
 				<el-col :lg="14" :xs="24">
@@ -75,7 +73,7 @@
 import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
 
 const { data } = useScreenData();
-const { isDemoField, isDemoSection } = useScreenFieldSource();
+const { isDemoSection } = useScreenFieldSource();
 const { textColor, mutedColor, splitColor, primary, baseGrid, axisStyle, tooltip } = useChartTheme();
 
 const trendKind = ref<'people' | 'times'>('people');
@@ -181,19 +179,12 @@
 </script>
 
 <style lang="scss" scoped>
+@import '../../styles/screen-kpi.scss';
+
 .screen-page {
-	.kpi-row,
 	.mid-row {
 		margin-bottom: 10px;
-	}
 
-	.kpi-row {
-		:deep(.el-col) {
-			margin-bottom: 10px;
-		}
-	}
-
-	.mid-row {
 		:deep(.panel-card) {
 			min-height: 360px;
 		}
diff --git a/src/modules/report/views/screen/resource.vue b/src/modules/report/views/screen/resource.vue
index 30b020a..61364cd 100644
--- a/src/modules/report/views/screen/resource.vue
+++ b/src/modules/report/views/screen/resource.vue
@@ -1,10 +1,8 @@
 <template>
 	<div class="screen-page">
-			<el-row :gutter="10" class="kpi-row">
-				<el-col v-for="item in kpis" :key="item.label" :lg="4" :sm="12" :xs="24">
-					<kpi-card v-bind="item" :demo="isDemoField(item.field)" />
-				</el-col>
-			</el-row>
+		<div class="kpi-grid" style="--kpi-cols: 5">
+			<kpi-card v-for="item in kpis" :key="item.label" v-bind="item" />
+		</div>
 
 			<el-row :gutter="10" class="mid-row">
 				<el-col :lg="10" :xs="24">
@@ -40,7 +38,7 @@
 import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
 
 const { data } = useScreenData();
-const { isDemoField, isDemoSection } = useScreenFieldSource();
+const { isDemoSection } = useScreenFieldSource();
 const { textColor, mutedColor, splitColor, primary, tooltip } = useChartTheme();
 
 const kpis = computed(() => [
@@ -131,19 +129,12 @@
 </script>
 
 <style lang="scss" scoped>
+@import '../../styles/screen-kpi.scss';
+
 .screen-page {
-	.kpi-row,
 	.mid-row {
 		margin-bottom: 10px;
-	}
 
-	.kpi-row {
-		:deep(.el-col) {
-			margin-bottom: 10px;
-		}
-	}
-
-	.mid-row {
 		:deep(.panel-card) {
 			min-height: 360px;
 		}

--
Gitblit v1.9.1