Require dual signatures for aftersales confirmation
This commit is contained in:
@@ -5,11 +5,12 @@ import './SignatureOverlay.css';
|
|||||||
|
|
||||||
interface SignatureOverlayProps {
|
interface SignatureOverlayProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
title?: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: (dataUrl: string) => void;
|
onConfirm: (dataUrl: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SignatureOverlay({ open, onCancel, onConfirm }: SignatureOverlayProps) {
|
function SignatureOverlay({ open, title = '请在框内签名', onCancel, onConfirm }: SignatureOverlayProps) {
|
||||||
const padRef = useRef<SignaturePadHandle>(null);
|
const padRef = useRef<SignaturePadHandle>(null);
|
||||||
const [, setData] = useState('');
|
const [, setData] = useState('');
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ function SignatureOverlay({ open, onCancel, onConfirm }: SignatureOverlayProps)
|
|||||||
<Button type="text" onClick={onCancel}>
|
<Button type="text" onClick={onCancel}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<span className="signature-overlay-title">请在框内签名</span>
|
<span className="signature-overlay-title">{title}</span>
|
||||||
<Button type="text" onClick={handleClear}>
|
<Button type="text" onClick={handleClear}>
|
||||||
清除
|
清除
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ function AftersalesConfirmPage() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
||||||
const [signatureData, setSignatureData] = useState('');
|
const [customerSignatureData, setCustomerSignatureData] = useState('');
|
||||||
const [showSignatureOverlay, setShowSignatureOverlay] = useState(false);
|
const [responsibleSignatureData, setResponsibleSignatureData] = useState('');
|
||||||
|
const [activeSignatureRole, setActiveSignatureRole] = useState<'customer' | 'responsible' | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const loadOrder = async () => {
|
const loadOrder = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -53,15 +56,20 @@ function AftersalesConfirmPage() {
|
|||||||
}, [serialNumber]);
|
}, [serialNumber]);
|
||||||
|
|
||||||
const handleAuthorize = async () => {
|
const handleAuthorize = async () => {
|
||||||
if (!signatureData) {
|
if (!customerSignatureData) {
|
||||||
message.error('请先签名');
|
message.error('请先完成客户签名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!responsibleSignatureData) {
|
||||||
|
message.error('请先完成负责人签名');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
||||||
action: 'authorize',
|
action: 'authorize',
|
||||||
signature: signatureData,
|
signature: customerSignatureData,
|
||||||
|
responsibleSignature: responsibleSignatureData,
|
||||||
});
|
});
|
||||||
setOrder(updated);
|
setOrder(updated);
|
||||||
message.success('感谢您的确认,工单已完成');
|
message.success('感谢您的确认,工单已完成');
|
||||||
@@ -99,8 +107,16 @@ function AftersalesConfirmPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearSignature = () => {
|
const handleClearSignature = (role: 'customer' | 'responsible') => {
|
||||||
setSignatureData('');
|
if (role === 'customer') {
|
||||||
|
setCustomerSignatureData('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setResponsibleSignatureData('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSignatureOverlay = (role: 'customer' | 'responsible') => {
|
||||||
|
setActiveSignatureRole(role);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -220,49 +236,104 @@ function AftersalesConfirmPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isClosed && order.signature && (
|
{isClosed && (order.signature || order.responsibleSignature) && (
|
||||||
<div className="aftersales-signature-archived">
|
<div className="aftersales-signature-archived">
|
||||||
<p className="aftersales-signature-tip">客户确认签名</p>
|
<div className="aftersales-signature-grid">
|
||||||
|
{order.signature && (
|
||||||
|
<div className="aftersales-signature-archived-item">
|
||||||
|
<p className="aftersales-signature-tip">客户签名</p>
|
||||||
<img
|
<img
|
||||||
src={order.signature}
|
src={order.signature}
|
||||||
alt="客户确认签名"
|
alt="客户签名"
|
||||||
className="aftersales-signature-archived-img"
|
className="aftersales-signature-archived-img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{order.responsibleSignature && (
|
||||||
|
<div className="aftersales-signature-archived-item">
|
||||||
|
<p className="aftersales-signature-tip">负责人签名</p>
|
||||||
|
<img
|
||||||
|
src={order.responsibleSignature}
|
||||||
|
alt="负责人签名"
|
||||||
|
className="aftersales-signature-archived-img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<>
|
<>
|
||||||
<div className="aftersales-signature-section">
|
<div className="aftersales-signature-section">
|
||||||
|
<p className="aftersales-signature-section-title">请签名确认维修结果</p>
|
||||||
|
<div className="aftersales-signature-grid">
|
||||||
|
<div className="aftersales-signature-item">
|
||||||
<div className="aftersales-signature-header">
|
<div className="aftersales-signature-header">
|
||||||
<p className="aftersales-signature-tip">请签名确认维修结果</p>
|
<p className="aftersales-signature-tip">客户签名</p>
|
||||||
{signatureData && (
|
{customerSignatureData && (
|
||||||
<Button size="small" type="link" onClick={handleClearSignature}>
|
<Button size="small" type="link" onClick={() => handleClearSignature('customer')}>
|
||||||
清除签名
|
清除
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{signatureData ? (
|
{customerSignatureData ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="aftersales-signature-preview"
|
className="aftersales-signature-preview"
|
||||||
onClick={() => setShowSignatureOverlay(true)}
|
onClick={() => openSignatureOverlay('customer')}
|
||||||
>
|
>
|
||||||
<img src={signatureData} alt="客户确认签名" />
|
<img src={customerSignatureData} alt="客户签名" />
|
||||||
<span className="aftersales-signature-preview-hint">点击可重新签名</span>
|
<span className="aftersales-signature-preview-hint">点击可重新签名</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="aftersales-signature-trigger"
|
className="aftersales-signature-trigger"
|
||||||
onClick={() => setShowSignatureOverlay(true)}
|
onClick={() => openSignatureOverlay('customer')}
|
||||||
>
|
>
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
<span>点击此处签名</span>
|
<span>客户签名</span>
|
||||||
<small>将进入签名页</small>
|
<small>将进入签名页</small>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="aftersales-signature-item">
|
||||||
|
<div className="aftersales-signature-header">
|
||||||
|
<p className="aftersales-signature-tip">负责人签名</p>
|
||||||
|
{responsibleSignatureData && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
onClick={() => handleClearSignature('responsible')}
|
||||||
|
>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{responsibleSignatureData ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="aftersales-signature-preview"
|
||||||
|
onClick={() => openSignatureOverlay('responsible')}
|
||||||
|
>
|
||||||
|
<img src={responsibleSignatureData} alt="负责人签名" />
|
||||||
|
<span className="aftersales-signature-preview-hint">点击可重新签名</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="aftersales-signature-trigger"
|
||||||
|
onClick={() => openSignatureOverlay('responsible')}
|
||||||
|
>
|
||||||
|
<EditOutlined />
|
||||||
|
<span>负责人签名</span>
|
||||||
|
<small>将进入签名页</small>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="aftersales-actions">
|
<div className="aftersales-actions">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
@@ -288,11 +359,16 @@ function AftersalesConfirmPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<SignatureOverlay
|
<SignatureOverlay
|
||||||
open={showSignatureOverlay}
|
open={activeSignatureRole !== null}
|
||||||
onCancel={() => setShowSignatureOverlay(false)}
|
title={activeSignatureRole === 'responsible' ? '负责人签名' : '客户签名'}
|
||||||
|
onCancel={() => setActiveSignatureRole(null)}
|
||||||
onConfirm={(url) => {
|
onConfirm={(url) => {
|
||||||
setSignatureData(url);
|
if (activeSignatureRole === 'responsible') {
|
||||||
setShowSignatureOverlay(false);
|
setResponsibleSignatureData(url);
|
||||||
|
} else {
|
||||||
|
setCustomerSignatureData(url);
|
||||||
|
}
|
||||||
|
setActiveSignatureRole(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -241,11 +241,11 @@ function AftersalesDetailPage() {
|
|||||||
.electronic-form-table td { min-height: 24px; }
|
.electronic-form-table td { min-height: 24px; }
|
||||||
.electronic-form-table .electronic-form-code { color: #165dff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 16px; font-weight: 700; }
|
.electronic-form-table .electronic-form-code { color: #165dff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 16px; font-weight: 700; }
|
||||||
.electronic-form-text { min-height: 72px; white-space: pre-wrap; }
|
.electronic-form-text { min-height: 72px; white-space: pre-wrap; }
|
||||||
.electronic-form-signature { display: flex; justify-content: flex-end; margin-top: 18px; }
|
.electronic-form-signatures { display: flex; justify-content: space-between; gap: 40px; margin-top: 22px; }
|
||||||
.electronic-form-signature-box { width: 240px; text-align: center; }
|
.electronic-form-signature-box { flex: 1; max-width: 300px; text-align: center; }
|
||||||
.electronic-form-signature-title { margin: 0 0 8px; font-weight: 600; }
|
.electronic-form-signature-title { margin: 0 0 8px; font-weight: 600; }
|
||||||
.electronic-form-signature-img { max-width: 220px; max-height: 90px; object-fit: contain; }
|
.electronic-form-signature-stage { height: 96px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #1f2937; }
|
||||||
.electronic-form-signature-line { height: 64px; border-bottom: 1px solid #1f2937; }
|
.electronic-form-signature-img { max-width: 240px; max-height: 90px; object-fit: contain; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>${formNode.outerHTML}</body>
|
<body>${formNode.outerHTML}</body>
|
||||||
@@ -611,18 +611,30 @@ function AftersalesDetailPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="electronic-form-signature">
|
<div className="electronic-form-signatures">
|
||||||
|
<div className="electronic-form-signature-box">
|
||||||
|
<p className="electronic-form-signature-title">负责人签名</p>
|
||||||
|
<div className="electronic-form-signature-stage">
|
||||||
|
{order.responsibleSignature ? (
|
||||||
|
<img
|
||||||
|
src={order.responsibleSignature}
|
||||||
|
alt="负责人签名"
|
||||||
|
className="electronic-form-signature-img"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="electronic-form-signature-box">
|
<div className="electronic-form-signature-box">
|
||||||
<p className="electronic-form-signature-title">客户签名</p>
|
<p className="electronic-form-signature-title">客户签名</p>
|
||||||
|
<div className="electronic-form-signature-stage">
|
||||||
{order.signature ? (
|
{order.signature ? (
|
||||||
<img
|
<img
|
||||||
src={order.signature}
|
src={order.signature}
|
||||||
alt="客户签名"
|
alt="客户签名"
|
||||||
className="electronic-form-signature-img"
|
className="electronic-form-signature-img"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<div className="electronic-form-signature-line" />
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,23 @@
|
|||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aftersales-signature-section-title {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aftersales-signature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aftersales-signature-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.aftersales-signature-header {
|
.aftersales-signature-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -53,6 +70,7 @@
|
|||||||
|
|
||||||
.aftersales-signature-trigger {
|
.aftersales-signature-trigger {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -88,6 +106,7 @@
|
|||||||
|
|
||||||
.aftersales-signature-preview {
|
.aftersales-signature-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -102,8 +121,9 @@
|
|||||||
|
|
||||||
.aftersales-signature-preview img {
|
.aftersales-signature-preview img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 160px;
|
max-height: 120px;
|
||||||
display: block;
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aftersales-signature-preview-hint {
|
.aftersales-signature-preview-hint {
|
||||||
@@ -115,12 +135,16 @@
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aftersales-signature-archived-item {
|
||||||
|
min-width: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aftersales-signature-archived-img {
|
.aftersales-signature-archived-img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 220px;
|
max-height: 160px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -137,3 +161,9 @@
|
|||||||
.aftersales-actions > button {
|
.aftersales-actions > button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.aftersales-signature-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,14 +78,16 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electronic-form-signature {
|
.electronic-form-signatures {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
margin-top: 18px;
|
gap: 40px;
|
||||||
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electronic-form-signature-box {
|
.electronic-form-signature-box {
|
||||||
width: 240px;
|
flex: 1;
|
||||||
|
max-width: 300px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +96,18 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electronic-form-signature-img {
|
.electronic-form-signature-stage {
|
||||||
max-width: 220px;
|
height: 96px;
|
||||||
max-height: 90px;
|
display: flex;
|
||||||
object-fit: contain;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.electronic-form-signature-line {
|
.electronic-form-signature-img {
|
||||||
height: 64px;
|
max-width: 240px;
|
||||||
border-bottom: 1px solid #1f2937;
|
max-height: 90px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
@@ -131,4 +136,8 @@
|
|||||||
.electronic-form-table td {
|
.electronic-form-table td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.electronic-form-signatures {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export interface AftersalesOrder {
|
|||||||
confirmedAt?: string;
|
confirmedAt?: string;
|
||||||
rejectCount: number;
|
rejectCount: number;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
|
responsibleSignature?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
technician?: User;
|
technician?: User;
|
||||||
@@ -217,6 +218,7 @@ export interface AftersalesPublicView {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
confirmedAt?: string;
|
confirmedAt?: string;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
|
responsibleSignature?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateAftersalesRequest {
|
export interface CreateAftersalesRequest {
|
||||||
@@ -257,5 +259,6 @@ export interface AftersalesListResponse {
|
|||||||
export interface CustomerConfirmRequest {
|
export interface CustomerConfirmRequest {
|
||||||
action: 'authorize' | 'reject';
|
action: 'authorize' | 'reject';
|
||||||
signature?: string;
|
signature?: string;
|
||||||
|
responsibleSignature?: string;
|
||||||
rejectReason?: string;
|
rejectReason?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user