feat(energy): rotate phone fullscreen drill tables for wider viewing
ci/woodpecker/push/woodpecker Pipeline was successful

Co-authored-by: HiFox Agent <agents-noreply@hifox.com>
This commit is contained in:
kfluous
2026-09-05 18:42:57 +08:00
co-authored by HiFox Agent
parent 7d792933dd
commit 89dca5c5a4
4 changed files with 99 additions and 14 deletions
@@ -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; }
@@ -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<HTMLElement>('[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<HTMLElement | null>(null);
const savedStyles = useRef(new Map<string, [string, string]>());
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<string, string> = {
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}
>
<span className="mobile-list-fullscreen-trigger__label">{isActive ? '退出宽表' : '查看完整表格'}</span>
<span className="mobile-list-fullscreen-trigger__label">{isActive ? '退出宽表' : isPhoneUserAgent(navigator.userAgent) ? '横屏查看' : '查看完整表格'}</span>
<Maximize2 size={15} aria-hidden />
</button>
);
@@ -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);
}
});
+8
View File
@@ -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;
}