From 89dca5c5a4e87d6b030928fed9c78f8580c15e4f Mon Sep 17 00:00:00 2001 From: kfluous Date: Sat, 5 Sep 2026 18:42:57 +0800 Subject: [PATCH] feat(energy): rotate phone fullscreen drill tables for wider viewing Co-authored-by: HiFox Agent --- .../energy/hydrogen-bi-v2/drill-workspace.css | 23 +++++++ .../common/MobileListFullscreenButton.tsx | 69 +++++++++++++++---- .../common/phone-viewport.test.ts | 13 ++++ .../lnbi-8113-exact/common/phone-viewport.ts | 8 +++ 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 src/vendor/lnbi-8113-exact/common/phone-viewport.test.ts create mode 100644 src/vendor/lnbi-8113-exact/common/phone-viewport.ts diff --git a/src/modules/energy/hydrogen-bi-v2/drill-workspace.css b/src/modules/energy/hydrogen-bi-v2/drill-workspace.css index e2a3fff..1f08d6f 100644 --- a/src/modules/energy/hydrogen-bi-v2/drill-workspace.css +++ b/src/modules/energy/hydrogen-bi-v2/drill-workspace.css @@ -1,4 +1,27 @@ /* Shared drill workspace: navigation stays visible, data owns the remaining height. */ +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] { + display: flex !important; + flex-direction: column !important; +} +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] > .ehb-modal-body { + flex: 1 1 0 !important; + min-height: 0 !important; + padding: 4px 8px !important; + gap: 3px !important; +} +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] .ehb-modal-table { + width: 100% !important; + min-width: 1050px !important; +} +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] .ehb-modal-table th, +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] .ehb-modal-table td { + padding: 7px 9px !important; + font-size: 11px !important; +} +html.ehb-landscape-session .ehb-modal-card.ehb-drill-modal--unified[data-mobile-fullscreen-mode="phone-rotated"] .ehb-drill-page-controls .ehb-btn { + min-height: 32px !important; + padding: 4px 10px !important; +} .ehb-drill-modal--unified .ehb-inline-child-row > td { background: #f8fbff; border-bottom: 1px solid #e2eaf4; } .ehb-drill-modal--unified .ehb-inline-child-row > td:first-child { background: #f8fbff !important; } .ehb-inline-child-name { display: flex; align-items: center; gap: 8px; min-width: 0; } diff --git a/src/vendor/lnbi-8113-exact/common/MobileListFullscreenButton.tsx b/src/vendor/lnbi-8113-exact/common/MobileListFullscreenButton.tsx index 60cce0c..ee77562 100644 --- a/src/vendor/lnbi-8113-exact/common/MobileListFullscreenButton.tsx +++ b/src/vendor/lnbi-8113-exact/common/MobileListFullscreenButton.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { isPhoneUserAgent, phoneRotation } from './phone-viewport'; import { Maximize2 } from 'lucide-react'; import './mobile-list-fullscreen.css'; @@ -10,21 +11,62 @@ export function MobileListFullscreenButton({ placement?: 'overlay' | 'inline'; }) { const [isActive, setIsActive] = useState(false); - useEffect(() => { - const resetTarget = () => { - document.querySelectorAll('[data-mobile-fullscreen-active="true"]').forEach((item) => { - item.removeAttribute('data-mobile-fullscreen-active'); - item.removeAttribute('data-mobile-fullscreen-mode'); - item.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback'); - }); + const targetRef = useRef(null); + const savedStyles = useRef(new Map()); + const restoreGeometry = () => { + const target = targetRef.current; + if (!target) return; + savedStyles.current.forEach(([value, priority], key) => { + if (value) target.style.setProperty(key, value, priority); else target.style.removeProperty(key); + }); + savedStyles.current.clear(); + }; + const updateGeometry = () => { + const target = targetRef.current; + if (!target) return; + restoreGeometry(); + const width = window.visualViewport?.width ?? window.innerWidth; + const height = window.visualViewport?.height ?? window.innerHeight; + const rotate = phoneRotation(navigator.userAgent, width, height); + target.dataset.mobileFullscreenMode = rotate ? 'phone-rotated' : 'native-scroll'; + if (!rotate) return; + const styles: Record = { + position: 'fixed', top: '0px', left: '0px', right: 'auto', bottom: 'auto', + width: `${height}px`, height: `${width}px`, 'max-width': 'none', 'max-height': 'none', + margin: '0px', transform: `translateX(${width}px) rotate(90deg)`, 'transform-origin': '0 0', + overflow: 'hidden', padding: '0px', }; + Object.entries(styles).forEach(([key, value]) => { + savedStyles.current.set(key, [target.style.getPropertyValue(key), target.style.getPropertyPriority(key)]); + target.style.setProperty(key, value, 'important'); + }); + }; + useEffect(() => { + const resize = () => updateGeometry(); + const escape = (event: KeyboardEvent) => { + if (event.key === 'Escape' && targetRef.current) { event.stopPropagation(); leaveLandscape(targetRef.current); } + }; + window.addEventListener('resize', resize); + window.visualViewport?.addEventListener('resize', resize); + document.addEventListener('keydown', escape, true); return () => { - document.documentElement.classList.remove('ehb-landscape-session'); - resetTarget(); + window.removeEventListener('resize', resize); + window.visualViewport?.removeEventListener('resize', resize); + document.removeEventListener('keydown', escape, true); + if (targetRef.current) { + restoreGeometry(); + targetRef.current.removeAttribute('data-mobile-fullscreen-active'); + targetRef.current.removeAttribute('data-mobile-fullscreen-mode'); + targetRef.current.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback'); + targetRef.current = null; + document.documentElement.classList.remove('ehb-landscape-session'); + } }; }, []); const leaveLandscape = (target: HTMLElement) => { + restoreGeometry(); + targetRef.current = null; target.removeAttribute('data-mobile-fullscreen-active'); target.removeAttribute('data-mobile-fullscreen-mode'); target.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback'); @@ -43,13 +85,12 @@ export function MobileListFullscreenButton({ } target.dataset.mobileFullscreenActive = 'true'; + targetRef.current = target; target.classList.add('is-mobile-list-fullscreen'); document.documentElement.classList.add('ehb-landscape-session'); setIsActive(true); - // 宽表始终在当前方向阅读:竖屏通过表格自身横向滚动,不请求全屏、 - // 不锁定方向,更不使用 CSS 旋转,避免 Safari 兼容路径把内容挤成窄条。 - target.dataset.mobileFullscreenMode = 'native-scroll'; + updateGeometry(); }; return ( @@ -61,7 +102,7 @@ export function MobileListFullscreenButton({ title={isActive ? '退出宽表模式' : label} onClick={openFullscreen} > - {isActive ? '退出宽表' : '查看完整表格'} + {isActive ? '退出宽表' : isPhoneUserAgent(navigator.userAgent) ? '横屏查看' : '查看完整表格'} ); diff --git a/src/vendor/lnbi-8113-exact/common/phone-viewport.test.ts b/src/vendor/lnbi-8113-exact/common/phone-viewport.test.ts new file mode 100644 index 0000000..5a6e110 --- /dev/null +++ b/src/vendor/lnbi-8113-exact/common/phone-viewport.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { isPhoneUserAgent, phoneRotation } from './phone-viewport.js'; +test('仅手机 UA 在竖屏宽表模式旋转,不影响平板和桌面', () => { + for (const ua of ['Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)', 'Mozilla/5.0 (Linux; Android 14) Mobile Safari/537.36']) { + assert.equal(isPhoneUserAgent(ua), true); + assert.equal(phoneRotation(ua, 390, 844), true); + assert.equal(phoneRotation(ua, 844, 390), false); + } + for (const ua of ['Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X)', 'Mozilla/5.0 (Macintosh; Intel Mac OS X)', 'Mozilla/5.0 (Linux; Android 14) Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)']) { + assert.equal(phoneRotation(ua, 390, 844), false); + } +}); diff --git a/src/vendor/lnbi-8113-exact/common/phone-viewport.ts b/src/vendor/lnbi-8113-exact/common/phone-viewport.ts new file mode 100644 index 0000000..f9d4319 --- /dev/null +++ b/src/vendor/lnbi-8113-exact/common/phone-viewport.ts @@ -0,0 +1,8 @@ +/** UA-based phone detection intentionally excludes iPad and Android tablets. */ +export function isPhoneUserAgent(ua: string): boolean { + return /iPhone|iPod|Windows Phone/i.test(ua) || (/Android/i.test(ua) && /Mobile/i.test(ua)); +} + +export function phoneRotation(ua: string, width: number, height: number): boolean { + return isPhoneUserAgent(ua) && height > width; +}