import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Card, Button, Spin, Result, message, Input } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, EditOutlined, UploadOutlined, } from '@ant-design/icons'; import { projectOrdersApi } from '@/services/api'; import type { ProjectOrderPublicView, ProjectType } from '@/types'; import PublicLayout, { PublicLogo } from '@/components/PublicLayout'; import SignatureOverlay from '@/components/SignatureOverlay'; import './styles/PublicQuery.css'; import './styles/AftersalesConfirm.css'; const PROJECT_TYPE_LABEL: Record = { survey: '现场勘查', implementation: '现场实施', maintenance: '项目维保', other: '其他', }; const SITE_IMAGE_MAX_EDGE = 1600; const SITE_IMAGE_QUALITY = 0.78; async function compressSiteImage(file: File): Promise { if (!file.type.startsWith('image/') || file.type === 'image/gif') { return file; } const imageUrl = URL.createObjectURL(file); try { const img = await loadImage(imageUrl); const scale = Math.min(1, SITE_IMAGE_MAX_EDGE / Math.max(img.width, img.height)); const width = Math.max(1, Math.round(img.width * scale)); const height = Math.max(1, Math.round(img.height * scale)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) return file; ctx.drawImage(img, 0, 0, width, height); const blob = await canvasToBlob(canvas, 'image/jpeg', SITE_IMAGE_QUALITY); if (!blob || blob.size >= file.size) { return file; } const name = file.name.replace(/\.[^.]+$/, '') || 'site-image'; return new File([blob], `${name}.jpg`, { type: 'image/jpeg', lastModified: Date.now(), }); } catch { return file; } finally { URL.revokeObjectURL(imageUrl); } } function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } function canvasToBlob( canvas: HTMLCanvasElement, type: string, quality: number, ): Promise { return new Promise((resolve) => canvas.toBlob(resolve, type, quality)); } function ProjectOrderCompletePage() { const { serialNumber = '' } = useParams<{ serialNumber: string }>(); const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const [uploadingImages, setUploadingImages] = useState(false); const [completionNote, setCompletionNote] = useState(''); const [engineerSignature, setEngineerSignature] = useState(''); const [signatureOpen, setSignatureOpen] = useState(false); const loadOrder = async () => { setLoading(true); setError(null); try { const data = await projectOrdersApi.publicQuery(serialNumber); setOrder(data); setCompletionNote(data.completionNote || ''); } catch (err: any) { setError(err?.response?.data?.message || err.message || '查询失败'); } finally { setLoading(false); } }; useEffect(() => { if (serialNumber) { loadOrder(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [serialNumber]); const handleUploadSiteImages = async (files: FileList | null) => { if (!files || files.length === 0) return; setUploadingImages(true); try { const compressedFiles = await Promise.all(Array.from(files).map(compressSiteImage)); const images = await projectOrdersApi.uploadSiteImages(serialNumber, compressedFiles); setOrder((prev) => (prev ? { ...prev, siteImages: images } : prev)); message.success('现场图片上传成功'); } catch (err: any) { message.error(err?.response?.data?.message || err.message || '上传现场图片失败'); } finally { setUploadingImages(false); } }; const handleComplete = async () => { if (!order) return; if (!order.siteImages || order.siteImages.length === 0) { message.error('请至少上传 1 张现场图片'); return; } if (!completionNote.trim()) { message.error('请填写完成说明'); return; } if (!engineerSignature) { message.error('请先完成工程师签名'); return; } setSubmitting(true); try { const updated = await projectOrdersApi.complete(serialNumber, { completionNote: completionNote.trim(), engineerSignature, }); setOrder(updated); message.success('项目工单已完成'); } catch (err: any) { message.error(err?.response?.data?.message || err.message || '提交完成失败'); } finally { setSubmitting(false); } }; if (loading) { return (

正在加载工单...

); } if (error || !order) { return ( } title="工单不存在" subTitle={error || '请检查您扫描的二维码是否正确'} /> ); } const isClosed = order.workOrderStatus === 'closed'; return (

项目工单完成

{order.serialNumber}

{isClosed ? ( } title="工单已完成" subTitle={ order.completedAt ? `完成时间:${new Date(order.completedAt).toLocaleString('zh-CN')}` : undefined } /> ) : ( } title="待现场完成" subTitle="请上传现场图片、填写完成说明并完成工程师签名" /> )}
公司名称 {order.companyName}
公司位置 {order.companyAddress}
现场联系人 {order.contactName}
项目类型 {PROJECT_TYPE_LABEL[order.projectType] || order.projectType}
现场情况说明 {order.siteDescription}
{order.completionNote && (
完成说明 {order.completionNote}
)} {order.technicianName && (
工程师 {order.technicianName}
)}
创建日期 {new Date(order.createdAt).toLocaleString('zh-CN')}
{order.siteImages && order.siteImages.length > 0 && (
现场图片
{order.siteImages.map((url) => ( 现场图片 ))}
)}
{isClosed && order.engineerSignature && (

工程师签名

工程师签名
)} {!isClosed && ( <>

现场图片

完成说明

setCompletionNote(e.target.value)} placeholder="请描述现场勘查、实施过程和最终完成情况" />

工程师签名

{engineerSignature ? ( 工程师签名 ) : ( 未签名 )}
)}
setSignatureOpen(false)} onConfirm={(dataUrl) => { setEngineerSignature(dataUrl); setSignatureOpen(false); }} />
); } export default ProjectOrderCompletePage;