Upload site images during aftersales confirmation
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
UploadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { aftersalesApi } from '@/services/api';
|
import { aftersalesApi } from '@/services/api';
|
||||||
import type { AftersalesPublicView, AftersalesServiceType } from '@/types';
|
import type { AftersalesPublicView, AftersalesServiceType } from '@/types';
|
||||||
@@ -34,6 +35,7 @@ function AftersalesConfirmPage() {
|
|||||||
const [activeSignatureRole, setActiveSignatureRole] = useState<'customer' | 'responsible' | null>(
|
const [activeSignatureRole, setActiveSignatureRole] = useState<'customer' | 'responsible' | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [uploadingImages, setUploadingImages] = useState(false);
|
||||||
|
|
||||||
const loadOrder = async () => {
|
const loadOrder = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -119,6 +121,20 @@ function AftersalesConfirmPage() {
|
|||||||
setActiveSignatureRole(role);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<PublicLayout>
|
<PublicLayout>
|
||||||
@@ -224,6 +240,18 @@ function AftersalesConfirmPage() {
|
|||||||
<span className="value value-block">{order.resolutionNote}</span>
|
<span className="value value-block">{order.resolutionNote}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{order.siteImages && order.siteImages.length > 0 && (
|
||||||
|
<div className="detail-item detail-item-block">
|
||||||
|
<span className="label">现场图片</span>
|
||||||
|
<div className="aftersales-site-images">
|
||||||
|
{order.siteImages.map((url) => (
|
||||||
|
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||||
|
<img src={url} alt="现场图片" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{order.technicianName && (
|
{order.technicianName && (
|
||||||
<div className="detail-item">
|
<div className="detail-item">
|
||||||
<span className="label">处理人</span>
|
<span className="label">处理人</span>
|
||||||
@@ -265,6 +293,33 @@ function AftersalesConfirmPage() {
|
|||||||
|
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<>
|
<>
|
||||||
|
<div className="aftersales-upload-section">
|
||||||
|
<p className="aftersales-signature-section-title">现场图片</p>
|
||||||
|
<label className="aftersales-upload-trigger">
|
||||||
|
<UploadOutlined />
|
||||||
|
<span>{uploadingImages ? '上传中...' : '上传现场图片'}</span>
|
||||||
|
<small>最多 6 张,单张不超过 5MB</small>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/heic,image/heif"
|
||||||
|
multiple
|
||||||
|
disabled={uploadingImages}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleUploadSiteImages(e.target.files);
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{order.siteImages && order.siteImages.length > 0 && (
|
||||||
|
<div className="aftersales-site-images">
|
||||||
|
{order.siteImages.map((url) => (
|
||||||
|
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||||
|
<img src={url} alt="现场图片" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="aftersales-signature-section">
|
<div className="aftersales-signature-section">
|
||||||
<p className="aftersales-signature-section-title">请签名确认维修结果</p>
|
<p className="aftersales-signature-section-title">请签名确认维修结果</p>
|
||||||
<div className="aftersales-signature-grid">
|
<div className="aftersales-signature-grid">
|
||||||
|
|||||||
@@ -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-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-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-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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>${formNode.outerHTML}</body>
|
<body>${formNode.outerHTML}</body>
|
||||||
@@ -618,6 +622,16 @@ function AftersalesDetailPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{order.siteImages && order.siteImages.length > 0 && (
|
||||||
|
<div className="electronic-form-site-images">
|
||||||
|
<p className="electronic-form-site-images-title">现场图片</p>
|
||||||
|
<div className="electronic-form-site-images-grid">
|
||||||
|
{order.siteImages.map((url) => (
|
||||||
|
<img key={url} src={url} alt="现场图片" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="electronic-form-signatures">
|
<div className="electronic-form-signatures">
|
||||||
<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>
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
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 {
|
.aftersales-signature-section-title {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
@@ -44,6 +50,58 @@
|
|||||||
font-weight: 600;
|
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 {
|
.aftersales-signature-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -115,6 +115,28 @@
|
|||||||
object-fit: contain;
|
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) {
|
@media (max-width: 720px) {
|
||||||
.electronic-form-header {
|
.electronic-form-header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -442,6 +442,19 @@ export const aftersalesApi = {
|
|||||||
}
|
}
|
||||||
throw new Error(response.data.error || '提交确认失败');
|
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 = {
|
export const usersApi = {
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export interface AftersalesOrder {
|
|||||||
rejectCount: number;
|
rejectCount: number;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
responsibleSignature?: string;
|
responsibleSignature?: string;
|
||||||
|
siteImages?: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
technician?: User;
|
technician?: User;
|
||||||
@@ -219,6 +220,7 @@ export interface AftersalesPublicView {
|
|||||||
confirmedAt?: string;
|
confirmedAt?: string;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
responsibleSignature?: string;
|
responsibleSignature?: string;
|
||||||
|
siteImages?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateAftersalesRequest {
|
export interface CreateAftersalesRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user