From a19204d4b56cfe9886770e955d79e6ff6a68de29 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Tue, 2 Jun 2026 11:04:38 +0800 Subject: [PATCH] Upload site images during aftersales confirmation --- src/pages/AftersalesConfirm.tsx | 55 ++++++++++++++++++++++++ src/pages/AftersalesDetail.tsx | 14 +++++++ src/pages/styles/AftersalesConfirm.css | 58 ++++++++++++++++++++++++++ src/pages/styles/AftersalesDetail.css | 22 ++++++++++ src/services/api.ts | 13 ++++++ src/types/index.ts | 2 + 6 files changed, 164 insertions(+) diff --git a/src/pages/AftersalesConfirm.tsx b/src/pages/AftersalesConfirm.tsx index d42a4dc..b9fe583 100644 --- a/src/pages/AftersalesConfirm.tsx +++ b/src/pages/AftersalesConfirm.tsx @@ -7,6 +7,7 @@ import { ClockCircleOutlined, ExclamationCircleOutlined, EditOutlined, + UploadOutlined, } from '@ant-design/icons'; import { aftersalesApi } from '@/services/api'; import type { AftersalesPublicView, AftersalesServiceType } from '@/types'; @@ -34,6 +35,7 @@ function AftersalesConfirmPage() { const [activeSignatureRole, setActiveSignatureRole] = useState<'customer' | 'responsible' | null>( null, ); + const [uploadingImages, setUploadingImages] = useState(false); const loadOrder = async () => { setLoading(true); @@ -119,6 +121,20 @@ function AftersalesConfirmPage() { setActiveSignatureRole(role); }; + const handleUploadSiteImages = async (files: FileList | null) => { + if (!files || files.length === 0) return; + setUploadingImages(true); + try { + const images = await aftersalesApi.uploadSiteImages(serialNumber, Array.from(files)); + setOrder((prev) => (prev ? { ...prev, siteImages: images } : prev)); + message.success('现场图片上传成功'); + } catch (err: any) { + message.error(err?.response?.data?.message || err.message || '上传现场图片失败'); + } finally { + setUploadingImages(false); + } + }; + if (loading) { return ( @@ -224,6 +240,18 @@ function AftersalesConfirmPage() { {order.resolutionNote} )} + {order.siteImages && order.siteImages.length > 0 && ( +
+ 现场图片 +
+ {order.siteImages.map((url) => ( + + 现场图片 + + ))} +
+
+ )} {order.technicianName && (
处理人 @@ -265,6 +293,33 @@ function AftersalesConfirmPage() { {isPending && ( <> +
+

现场图片

+ + {order.siteImages && order.siteImages.length > 0 && ( +
+ {order.siteImages.map((url) => ( + + 现场图片 + + ))} +
+ )} +

请签名确认维修结果

diff --git a/src/pages/AftersalesDetail.tsx b/src/pages/AftersalesDetail.tsx index 5248ed3..f420195 100644 --- a/src/pages/AftersalesDetail.tsx +++ b/src/pages/AftersalesDetail.tsx @@ -251,6 +251,10 @@ function AftersalesDetailPage() { .electronic-form-signature-title { flex: 0 0 auto; margin: 0 10px 0 0; font-weight: 600; white-space: nowrap; } .electronic-form-signature-stage { flex: 1; height: 72px; min-width: 160px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #1f2937; } .electronic-form-signature-img { max-width: 180px; max-height: 68px; object-fit: contain; } + .electronic-form-site-images { margin-top: 18px; } + .electronic-form-site-images-title { margin: 0 0 10px; font-weight: 600; } + .electronic-form-site-images-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } + .electronic-form-site-images-grid img { width: 100%; height: 150px; object-fit: cover; border: 1px solid #1f2937; } ${formNode.outerHTML} @@ -618,6 +622,16 @@ function AftersalesDetailPage() { + {order.siteImages && order.siteImages.length > 0 && ( +
+

现场图片

+
+ {order.siteImages.map((url) => ( + 现场图片 + ))} +
+
+ )}

处理人签名:

diff --git a/src/pages/styles/AftersalesConfirm.css b/src/pages/styles/AftersalesConfirm.css index 0564137..e35a1c4 100644 --- a/src/pages/styles/AftersalesConfirm.css +++ b/src/pages/styles/AftersalesConfirm.css @@ -37,6 +37,12 @@ border-top: 1px solid rgba(0, 0, 0, 0.06); } +.aftersales-upload-section { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid rgba(0, 0, 0, 0.06); +} + .aftersales-signature-section-title { margin: 0 0 14px; color: #111827; @@ -44,6 +50,58 @@ font-weight: 600; } +.aftersales-upload-trigger { + min-height: 116px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + border: 1.5px dashed #94a3b8; + border-radius: 12px; + background: #f8fafc; + color: #1f2937; + cursor: pointer; +} + +.aftersales-upload-trigger input { + display: none; +} + +.aftersales-upload-trigger .anticon { + color: #1677ff; + font-size: 22px; +} + +.aftersales-upload-trigger small { + color: #6b7280; + font-size: 12px; +} + +.aftersales-site-images { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); + gap: 8px; + width: 100%; + margin-top: 12px; +} + +.aftersales-site-images a { + display: block; + aspect-ratio: 1; + overflow: hidden; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #ffffff; +} + +.aftersales-site-images img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + .aftersales-signature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/src/pages/styles/AftersalesDetail.css b/src/pages/styles/AftersalesDetail.css index 2203099..f8be27d 100644 --- a/src/pages/styles/AftersalesDetail.css +++ b/src/pages/styles/AftersalesDetail.css @@ -115,6 +115,28 @@ object-fit: contain; } +.electronic-form-site-images { + margin-top: 18px; +} + +.electronic-form-site-images-title { + margin: 0 0 10px; + font-weight: 600; +} + +.electronic-form-site-images-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.electronic-form-site-images-grid img { + width: 100%; + height: 150px; + object-fit: cover; + border: 1px solid #1f2937; +} + @media (max-width: 720px) { .electronic-form-header { align-items: flex-start; diff --git a/src/services/api.ts b/src/services/api.ts index 0d96587..b502bc9 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -442,6 +442,19 @@ export const aftersalesApi = { } throw new Error(response.data.error || '提交确认失败'); }, + + uploadSiteImages: async (serialNumber: string, files: File[]) => { + const formData = new FormData(); + files.forEach((file) => formData.append('files', file)); + const response = await apiClient.post( + `/aftersales/${encodeURIComponent(serialNumber)}/site-images`, + formData, + ); + if (response.data.siteImages) { + return response.data.siteImages as string[]; + } + throw new Error(response.data.error || '上传现场图片失败'); + }, }; export const usersApi = { diff --git a/src/types/index.ts b/src/types/index.ts index 333dfe4..d5d1b39 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -198,6 +198,7 @@ export interface AftersalesOrder { rejectCount: number; signature?: string; responsibleSignature?: string; + siteImages?: string[]; createdAt: string; updatedAt: string; technician?: User; @@ -219,6 +220,7 @@ export interface AftersalesPublicView { confirmedAt?: string; signature?: string; responsibleSignature?: string; + siteImages?: string[]; } export interface CreateAftersalesRequest {