feat(energy): rotate phone fullscreen drill tables for wider viewing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-authored-by: HiFox Agent <agents-noreply@hifox.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user