feat: 小程序 webview 内点全屏监控自动 CSS 横屏,版本号 1.1.4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

小程序 webview 无法调用系统旋转 API,竖屏全屏体验很差。检测到微信/抖音/
支付宝小程序 UA 且当前为竖屏时,全屏覆盖层用 transform: rotate(90deg)
配合 100vh × 100vw 的尺寸模拟真横屏,用户用横屏姿势看设备即可获得横屏
监控面板。浏览器会自动把触摸坐标映射回旋转前坐标系,交互不受影响。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-15 18:42:43 +08:00
parent f3b795e8a9
commit 26d59190c9
2 changed files with 34 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"private": true, "private": true,
"version": "1.1.3", "version": "1.1.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",

View File

@@ -7,6 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types'; import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
import { fetchMonitoring } from './api'; import { fetchMonitoring } from './api';
import Blur from '../../components/Blur';
const SearchableSelect = ({ const SearchableSelect = ({
options, options,
@@ -278,6 +279,20 @@ export default function MonitoringView() {
return () => { document.body.style.overflow = ''; }; return () => { document.body.style.overflow = ''; };
}, [isFullscreen]); }, [isFullscreen]);
// 检测是否在小程序 webview 中(微信/抖音/支付宝等),且当前是竖屏
// 小程序 webview 无法调用系统旋转 API只能用 CSS rotate 强制横屏
const forceLandscape = useMemo(() => {
if (typeof window === 'undefined') return false;
const ua = navigator.userAgent || '';
const isMiniProgram =
/miniProgram/i.test(ua) ||
/toutiaomicroapp/i.test(ua) ||
/AlipayClient/i.test(ua) ||
(window as any).__wxjs_environment === 'miniprogram';
const isPortrait = window.innerHeight > window.innerWidth;
return isMiniProgram && isPortrait;
}, [isFullscreen]);
return ( return (
<> <>
{/* 顶部哨兵:离开视口时显示回到顶部按钮 */} {/* 顶部哨兵:离开视口时显示回到顶部按钮 */}
@@ -290,7 +305,20 @@ export default function MonitoringView() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-slate-950 flex flex-col overflow-hidden" className="fixed z-[100] bg-slate-950 flex flex-col overflow-hidden"
style={
forceLandscape
? {
// 小程序 webview 无法真横屏,强制 CSS 旋转 90 度模拟横屏
top: 0,
left: '100vw',
width: '100vh',
height: '100vw',
transform: 'rotate(90deg)',
transformOrigin: 'top left',
}
: { top: 0, left: 0, right: 0, bottom: 0 }
}
> >
{/* Top bar: compact inline KPI */} {/* Top bar: compact inline KPI */}
<div className="flex-shrink-0 px-3 py-2 border-b border-slate-800/60 flex items-center justify-between"> <div className="flex-shrink-0 px-3 py-2 border-b border-slate-800/60 flex items-center justify-between">
@@ -458,8 +486,8 @@ export default function MonitoringView() {
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
<div className={`w-2 h-2 rounded-full mx-auto ${v.isOnline ? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]' : v.isDataSynced ? 'bg-slate-600' : 'bg-amber-400 animate-pulse'}`}></div> <div className={`w-2 h-2 rounded-full mx-auto ${v.isOnline ? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]' : v.isDataSynced ? 'bg-slate-600' : 'bg-amber-400 animate-pulse'}`}></div>
</td> </td>
<td className="px-3 py-2 text-xs font-bold text-white">{v.plate}</td> <td className="px-3 py-2 text-xs font-bold text-white"><Blur>{v.plate}</Blur></td>
<td className="px-3 py-2 text-[11px] text-slate-400">{v.customer || '-'}</td> <td className="px-3 py-2 text-[11px] text-slate-400"><Blur>{v.customer || '-'}</Blur></td>
<td className="px-3 py-2 text-[11px] text-slate-400">{v.rentStatus || '-'}</td> <td className="px-3 py-2 text-[11px] text-slate-400">{v.rentStatus || '-'}</td>
<td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td> <td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
<td className="px-3 py-2 text-right"> <td className="px-3 py-2 text-right">
@@ -829,14 +857,14 @@ export default function MonitoringView() {
</div> </div>
<div className="overflow-hidden flex-1"> <div className="overflow-hidden flex-1">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-xs font-black text-slate-900 font-mono">{v.plate}</span> <span className="text-xs font-black text-slate-900 font-mono"><Blur>{v.plate}</Blur></span>
<span className={`text-[8px] px-1 rounded ${v.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}> <span className={`text-[8px] px-1 rounded ${v.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
{v.isOnline ? '在线' : '离线'} {v.isOnline ? '在线' : '离线'}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-[8px] text-slate-300 font-bold">{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''}</span> <span className="text-[8px] text-slate-300 font-bold">{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''}</span>
<span className="text-[9px] font-bold text-slate-600 truncate">{v.customer || '-'}</span> <span className="text-[9px] font-bold text-slate-600 truncate"><Blur>{v.customer || '-'}</Blur></span>
</div> </div>
</div> </div>
</div> </div>