Open signature in a landscape overlay and fix mobile touch

The inline signature pad on the confirm page was unusable on phones:
the canvas had a fixed 480px internal width but shrank via max-width
on small screens, so pointer coordinates landed in only the top-left
fraction of the drawing buffer. Edge-of-screen strokes also collided
with iOS Safari back-swipe.

Tapping the new signature trigger now opens a full-screen overlay
that rotates to landscape (via 100dvh/100dvw + CSS rotate on portrait
phones, plus an opportunistic screen.orientation.lock) so customers
get the widest possible signing area, away from the system edge.

Also swap the hand-rolled drawing logic for signature_pad, with a
ResizeObserver-driven resize that preserves strokes via
toData/fromData and scales the canvas to devicePixelRatio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frudrax Cheng
2026-05-27 09:11:05 +08:00
parent 1d944b0fd3
commit ab5acbc452
7 changed files with 310 additions and 120 deletions
+40 -13
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Card, Button, Spin, Result, message, Modal, Input } from 'antd';
import {
@@ -6,11 +6,12 @@ import {
CloseCircleOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
EditOutlined,
} from '@ant-design/icons';
import { aftersalesApi } from '@/services/api';
import type { AftersalesPublicView, AftersalesServiceType } from '@/types';
import PublicLayout, { PublicLogo } from '@/components/PublicLayout';
import SignaturePad, { type SignaturePadHandle } from '@/components/SignaturePad';
import SignatureOverlay from '@/components/SignatureOverlay';
import './styles/PublicQuery.css';
import './styles/AftersalesConfirm.css';
@@ -29,8 +30,7 @@ function AftersalesConfirmPage() {
const [rejectReason, setRejectReason] = useState('');
const [showRejectDialog, setShowRejectDialog] = useState(false);
const [signatureData, setSignatureData] = useState('');
const signatureRef = useRef<SignaturePadHandle>(null);
const [showSignatureOverlay, setShowSignatureOverlay] = useState(false);
const loadOrder = async () => {
setLoading(true);
@@ -53,7 +53,7 @@ function AftersalesConfirmPage() {
}, [serialNumber]);
const handleAuthorize = async () => {
if (signatureRef.current?.isEmpty() || !signatureData) {
if (!signatureData) {
message.error('请先签名');
return;
}
@@ -100,7 +100,6 @@ function AftersalesConfirmPage() {
};
const handleClearSignature = () => {
signatureRef.current?.clear();
setSignatureData('');
};
@@ -236,14 +235,33 @@ function AftersalesConfirmPage() {
<>
<div className="aftersales-signature-section">
<div className="aftersales-signature-header">
<p className="aftersales-signature-tip"></p>
<Button size="small" type="link" onClick={handleClearSignature}>
</Button>
</div>
<div className="aftersales-signature-canvas-wrap">
<SignaturePad ref={signatureRef} onChange={setSignatureData} />
<p className="aftersales-signature-tip"></p>
{signatureData && (
<Button size="small" type="link" onClick={handleClearSignature}>
</Button>
)}
</div>
{signatureData ? (
<button
type="button"
className="aftersales-signature-preview"
onClick={() => setShowSignatureOverlay(true)}
>
<img src={signatureData} alt="客户签名" />
<span className="aftersales-signature-preview-hint"></span>
</button>
) : (
<button
type="button"
className="aftersales-signature-trigger"
onClick={() => setShowSignatureOverlay(true)}
>
<EditOutlined />
<span></span>
<small></small>
</button>
)}
</div>
<div className="aftersales-actions">
<Button
@@ -269,6 +287,15 @@ function AftersalesConfirmPage() {
)}
</Card>
<SignatureOverlay
open={showSignatureOverlay}
onCancel={() => setShowSignatureOverlay(false)}
onConfirm={(url) => {
setSignatureData(url);
setShowSignatureOverlay(false);
}}
/>
<Modal
title="请说明退回原因"
open={showRejectDialog}