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. */
|
/* 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 { background: #f8fbff; border-bottom: 1px solid #e2eaf4; }
|
||||||
.ehb-drill-modal--unified .ehb-inline-child-row > td:first-child { background: #f8fbff !important; }
|
.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; }
|
.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 { Maximize2 } from 'lucide-react';
|
||||||
import './mobile-list-fullscreen.css';
|
import './mobile-list-fullscreen.css';
|
||||||
|
|
||||||
@@ -10,21 +11,62 @@ export function MobileListFullscreenButton({
|
|||||||
placement?: 'overlay' | 'inline';
|
placement?: 'overlay' | 'inline';
|
||||||
}) {
|
}) {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
useEffect(() => {
|
const targetRef = useRef<HTMLElement | null>(null);
|
||||||
const resetTarget = () => {
|
const savedStyles = useRef(new Map<string, [string, string]>());
|
||||||
document.querySelectorAll<HTMLElement>('[data-mobile-fullscreen-active="true"]').forEach((item) => {
|
const restoreGeometry = () => {
|
||||||
item.removeAttribute('data-mobile-fullscreen-active');
|
const target = targetRef.current;
|
||||||
item.removeAttribute('data-mobile-fullscreen-mode');
|
if (!target) return;
|
||||||
item.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback');
|
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 () => {
|
return () => {
|
||||||
document.documentElement.classList.remove('ehb-landscape-session');
|
window.removeEventListener('resize', resize);
|
||||||
resetTarget();
|
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) => {
|
const leaveLandscape = (target: HTMLElement) => {
|
||||||
|
restoreGeometry();
|
||||||
|
targetRef.current = null;
|
||||||
target.removeAttribute('data-mobile-fullscreen-active');
|
target.removeAttribute('data-mobile-fullscreen-active');
|
||||||
target.removeAttribute('data-mobile-fullscreen-mode');
|
target.removeAttribute('data-mobile-fullscreen-mode');
|
||||||
target.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback');
|
target.classList.remove('is-mobile-list-fullscreen', 'is-mobile-list-fallback');
|
||||||
@@ -43,13 +85,12 @@ export function MobileListFullscreenButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
target.dataset.mobileFullscreenActive = 'true';
|
target.dataset.mobileFullscreenActive = 'true';
|
||||||
|
targetRef.current = target;
|
||||||
target.classList.add('is-mobile-list-fullscreen');
|
target.classList.add('is-mobile-list-fullscreen');
|
||||||
document.documentElement.classList.add('ehb-landscape-session');
|
document.documentElement.classList.add('ehb-landscape-session');
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
|
|
||||||
// 宽表始终在当前方向阅读:竖屏通过表格自身横向滚动,不请求全屏、
|
updateGeometry();
|
||||||
// 不锁定方向,更不使用 CSS 旋转,避免 Safari 兼容路径把内容挤成窄条。
|
|
||||||
target.dataset.mobileFullscreenMode = 'native-scroll';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +102,7 @@ export function MobileListFullscreenButton({
|
|||||||
title={isActive ? '退出宽表模式' : label}
|
title={isActive ? '退出宽表模式' : label}
|
||||||
onClick={openFullscreen}
|
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 />
|
<Maximize2 size={15} aria-hidden />
|
||||||
</button>
|
</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