Use real device rotation for signature overlay
CSS transform: rotate(90deg) only rotates the visual; pointer events
still fire in the unrotated viewport coordinate system, and
canvas.getBoundingClientRect returns the rotated bounding box with
swapped width/height. signature_pad computed stroke positions from
clientX/clientY minus that swapped rect, so most strokes landed
outside the canvas drawing buffer. The brief flash of marks visible
when the phone was rotated back to portrait was the small fraction of
points that happened to land inside the canvas, revealed once the CSS
rotation was undone.
Drop the CSS fake-landscape and gate the signature pad behind real
device orientation: portrait shows a rotate-your-phone prompt, and
the pad only renders in landscape where the coordinate system is
clean. Attempt screen.orientation.lock('landscape') where supported;
iOS users with portrait lock see the prompt with a hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,19 +17,6 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Phones in portrait: rotate the whole stage to use landscape space */
|
||||
@media (orientation: portrait) and (max-width: 820px) {
|
||||
.signature-overlay-stage {
|
||||
inset: auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100dvh;
|
||||
height: 100dvw;
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
|
||||
.signature-overlay-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -50,7 +37,6 @@
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
/* keep room from screen edges so iOS back-swipe doesn't intercept */
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
@@ -73,3 +59,41 @@
|
||||
.signature-overlay-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.signature-overlay-rotate {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
gap: 14px;
|
||||
background: #f8fafc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signature-overlay-rotate-icon {
|
||||
font-size: 64px;
|
||||
color: #1677ff;
|
||||
animation: signature-rotate-hint 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes signature-rotate-hint {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(90deg); }
|
||||
}
|
||||
|
||||
.signature-overlay-rotate-text {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.signature-overlay-rotate-hint {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { RotateRightOutlined } from '@ant-design/icons';
|
||||
import SignaturePad, { type SignaturePadHandle } from './SignaturePad';
|
||||
import './SignatureOverlay.css';
|
||||
|
||||
@@ -9,26 +10,38 @@ interface SignatureOverlayProps {
|
||||
onConfirm: (dataUrl: string) => void;
|
||||
}
|
||||
|
||||
const isLandscapeNow = () =>
|
||||
typeof window !== 'undefined' && window.matchMedia('(orientation: landscape)').matches;
|
||||
|
||||
function SignatureOverlay({ open, onCancel, onConfirm }: SignatureOverlayProps) {
|
||||
const padRef = useRef<SignaturePadHandle>(null);
|
||||
const [data, setData] = useState('');
|
||||
const [, setData] = useState('');
|
||||
const [landscape, setLandscape] = useState<boolean>(isLandscapeNow);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const mq = window.matchMedia('(orientation: landscape)');
|
||||
const update = () => setLandscape(mq.matches);
|
||||
update();
|
||||
mq.addEventListener('change', update);
|
||||
|
||||
const orientation = (screen as Screen & {
|
||||
orientation?: { lock?: (o: string) => Promise<void> };
|
||||
orientation?: { lock?: (o: string) => Promise<void>; unlock?: () => void };
|
||||
}).orientation;
|
||||
if (orientation?.lock) {
|
||||
orientation.lock('landscape').catch(() => {
|
||||
// ignore; iOS Safari etc. don't permit lock outside fullscreen
|
||||
// ignore; iOS Safari and most browsers refuse outside fullscreen
|
||||
});
|
||||
}
|
||||
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
mq.removeEventListener('change', update);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
orientation?.unlock?.();
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
@@ -40,15 +53,29 @@ function SignatureOverlay({ open, onCancel, onConfirm }: SignatureOverlayProps)
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (padRef.current?.isEmpty()) return;
|
||||
const url = padRef.current?.getDataURL() || data;
|
||||
if (!url) return;
|
||||
onConfirm(url);
|
||||
const pad = padRef.current;
|
||||
if (!pad || pad.isEmpty()) return;
|
||||
onConfirm(pad.getDataURL());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="signature-overlay" role="dialog" aria-modal="true">
|
||||
<div className="signature-overlay-stage">
|
||||
{!landscape && (
|
||||
<div className="signature-overlay-rotate">
|
||||
<RotateRightOutlined className="signature-overlay-rotate-icon" />
|
||||
<p className="signature-overlay-rotate-text">请将手机横置以开始签名</p>
|
||||
<p className="signature-overlay-rotate-hint">
|
||||
如果无法旋转,请检查手机是否锁定了竖屏方向
|
||||
</p>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* SignaturePad stays mounted so its strokes survive accidental rotation flips */}
|
||||
<div
|
||||
className="signature-overlay-stage"
|
||||
style={{ visibility: landscape ? 'visible' : 'hidden' }}
|
||||
aria-hidden={!landscape}
|
||||
>
|
||||
<div className="signature-overlay-bar">
|
||||
<Button type="text" onClick={onCancel}>
|
||||
取消
|
||||
|
||||
Reference in New Issue
Block a user