397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { Card, Button, Spin, Result, message, Modal, Input } from 'antd';
|
|
import {
|
|
CheckCircleOutlined,
|
|
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 SignatureOverlay from '@/components/SignatureOverlay';
|
|
import './styles/PublicQuery.css';
|
|
import './styles/AftersalesConfirm.css';
|
|
|
|
const SERVICE_TYPE_LABEL: Record<AftersalesServiceType, string> = {
|
|
software: '软件故障',
|
|
hardware: '硬件故障',
|
|
maintenance: '售后维保',
|
|
};
|
|
|
|
function AftersalesConfirmPage() {
|
|
const { serialNumber = '' } = useParams<{ serialNumber: string }>();
|
|
const [order, setOrder] = useState<AftersalesPublicView | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [rejectReason, setRejectReason] = useState('');
|
|
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
|
const [customerSignatureData, setCustomerSignatureData] = useState('');
|
|
const [responsibleSignatureData, setResponsibleSignatureData] = useState('');
|
|
const [activeSignatureRole, setActiveSignatureRole] = useState<'customer' | 'responsible' | null>(
|
|
null,
|
|
);
|
|
|
|
const loadOrder = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const data = await aftersalesApi.publicQuery(serialNumber);
|
|
setOrder(data);
|
|
} 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 handleAuthorize = async () => {
|
|
if (!customerSignatureData) {
|
|
message.error('请先完成客户签名');
|
|
return;
|
|
}
|
|
if (!responsibleSignatureData) {
|
|
message.error('请先完成负责人签名');
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
|
action: 'authorize',
|
|
signature: customerSignatureData,
|
|
responsibleSignature: responsibleSignatureData,
|
|
});
|
|
setOrder(updated);
|
|
message.success('感谢您的确认,工单已完成');
|
|
} catch (err: any) {
|
|
message.error(err?.response?.data?.message || err.message || '提交失败');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const openRejectDialog = () => {
|
|
setRejectReason('');
|
|
setShowRejectDialog(true);
|
|
};
|
|
|
|
const confirmReject = async () => {
|
|
const reason = rejectReason.trim();
|
|
if (!reason) {
|
|
message.error('请填写退回原因');
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
|
action: 'reject',
|
|
rejectReason: reason,
|
|
});
|
|
setOrder(updated);
|
|
setShowRejectDialog(false);
|
|
message.success('已通知技术员重新处理');
|
|
} catch (err: any) {
|
|
message.error(err?.response?.data?.message || err.message || '提交失败');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleClearSignature = (role: 'customer' | 'responsible') => {
|
|
if (role === 'customer') {
|
|
setCustomerSignatureData('');
|
|
return;
|
|
}
|
|
setResponsibleSignatureData('');
|
|
};
|
|
|
|
const openSignatureOverlay = (role: 'customer' | 'responsible') => {
|
|
setActiveSignatureRole(role);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<PublicLayout>
|
|
<Card className="query-card" bordered={false}>
|
|
<div className="loading-container">
|
|
<Spin size="large" />
|
|
<p>正在加载工单...</p>
|
|
</div>
|
|
</Card>
|
|
</PublicLayout>
|
|
);
|
|
}
|
|
|
|
if (error || !order) {
|
|
return (
|
|
<PublicLayout>
|
|
<Card className="query-card" bordered={false}>
|
|
<Result
|
|
icon={<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '72px' }} />}
|
|
title="工单不存在"
|
|
subTitle={error || '请检查您扫描的二维码是否正确'}
|
|
/>
|
|
</Card>
|
|
</PublicLayout>
|
|
);
|
|
}
|
|
|
|
const isPending = order.workOrderStatus === 'pending_confirmation';
|
|
const isClosed = order.workOrderStatus === 'closed';
|
|
const isRejected = order.workOrderStatus === 'rejected';
|
|
const isCreated = order.workOrderStatus === 'created';
|
|
|
|
const renderStatusBanner = () => {
|
|
if (isClosed) {
|
|
return (
|
|
<Result
|
|
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '64px' }} />}
|
|
title="工单已完成"
|
|
subTitle={
|
|
order.confirmedAt
|
|
? `确认时间:${new Date(order.confirmedAt).toLocaleString('zh-CN')}`
|
|
: undefined
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
if (isRejected) {
|
|
return (
|
|
<Result
|
|
icon={<ExclamationCircleOutlined style={{ color: '#faad14', fontSize: '64px' }} />}
|
|
title="工单已退回"
|
|
subTitle="技术员将重新处理,请耐心等待"
|
|
/>
|
|
);
|
|
}
|
|
if (isCreated) {
|
|
return (
|
|
<Result
|
|
icon={<ClockCircleOutlined style={{ color: '#1677ff', fontSize: '64px' }} />}
|
|
title="工单处理中"
|
|
subTitle="技术员尚未提交客户确认,请稍后再来查看"
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<PublicLayout>
|
|
<Card className="query-card aftersales-confirm-card" bordered={false}>
|
|
<div className="query-header">
|
|
<PublicLogo />
|
|
<h1 className="aftersales-title">售后服务确认</h1>
|
|
<p className="aftersales-serial">{order.serialNumber}</p>
|
|
</div>
|
|
|
|
{renderStatusBanner()}
|
|
|
|
<div className="result-details aftersales-details">
|
|
<div className="detail-item">
|
|
<span className="label">公司名称</span>
|
|
<span className="value">{order.companyName}</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="label">公司位置</span>
|
|
<span className="value">{order.companyAddress}</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="label">企业联系人</span>
|
|
<span className="value">{order.contactName}</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="label">服务类型</span>
|
|
<span className="value">{SERVICE_TYPE_LABEL[order.serviceType] || order.serviceType}</span>
|
|
</div>
|
|
<div className="detail-item detail-item-block">
|
|
<span className="label">问题描述反馈</span>
|
|
<span className="value value-block">{order.issueDescription}</span>
|
|
</div>
|
|
{order.resolutionNote && (
|
|
<div className="detail-item detail-item-block">
|
|
<span className="label">处理结果</span>
|
|
<span className="value value-block">{order.resolutionNote}</span>
|
|
</div>
|
|
)}
|
|
{order.technicianName && (
|
|
<div className="detail-item">
|
|
<span className="label">处理人</span>
|
|
<span className="value">{order.technicianName}</span>
|
|
</div>
|
|
)}
|
|
<div className="detail-item">
|
|
<span className="label">创建日期</span>
|
|
<span className="value">{new Date(order.createdAt).toLocaleString('zh-CN')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{isClosed && (order.signature || order.responsibleSignature) && (
|
|
<div className="aftersales-signature-archived">
|
|
<div className="aftersales-signature-grid">
|
|
{order.signature && (
|
|
<div className="aftersales-signature-archived-item">
|
|
<p className="aftersales-signature-tip">客户签名</p>
|
|
<img
|
|
src={order.signature}
|
|
alt="客户签名"
|
|
className="aftersales-signature-archived-img"
|
|
/>
|
|
</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 && (
|
|
<>
|
|
<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">
|
|
<p className="aftersales-signature-tip">客户签名</p>
|
|
{customerSignatureData && (
|
|
<Button size="small" type="link" onClick={() => handleClearSignature('customer')}>
|
|
清除
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{customerSignatureData ? (
|
|
<button
|
|
type="button"
|
|
className="aftersales-signature-preview"
|
|
onClick={() => openSignatureOverlay('customer')}
|
|
>
|
|
<img src={customerSignatureData} alt="客户签名" />
|
|
<span className="aftersales-signature-preview-hint">点击可重新签名</span>
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="aftersales-signature-trigger"
|
|
onClick={() => openSignatureOverlay('customer')}
|
|
>
|
|
<EditOutlined />
|
|
<span>客户签名</span>
|
|
<small>将进入签名页</small>
|
|
</button>
|
|
)}
|
|
</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">
|
|
<Button
|
|
size="large"
|
|
danger
|
|
block
|
|
loading={submitting}
|
|
onClick={openRejectDialog}
|
|
>
|
|
未授权
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
size="large"
|
|
block
|
|
loading={submitting}
|
|
onClick={handleAuthorize}
|
|
>
|
|
已授权
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Card>
|
|
|
|
<SignatureOverlay
|
|
open={activeSignatureRole !== null}
|
|
title={activeSignatureRole === 'responsible' ? '负责人签名' : '客户签名'}
|
|
onCancel={() => setActiveSignatureRole(null)}
|
|
onConfirm={(url) => {
|
|
if (activeSignatureRole === 'responsible') {
|
|
setResponsibleSignatureData(url);
|
|
} else {
|
|
setCustomerSignatureData(url);
|
|
}
|
|
setActiveSignatureRole(null);
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title="请说明退回原因"
|
|
open={showRejectDialog}
|
|
onCancel={() => setShowRejectDialog(false)}
|
|
onOk={confirmReject}
|
|
okText="确认退回"
|
|
cancelText="取消"
|
|
confirmLoading={submitting}
|
|
>
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder="请简要说明未授权的原因,方便技术员改进"
|
|
value={rejectReason}
|
|
onChange={(e) => setRejectReason(e.target.value)}
|
|
maxLength={200}
|
|
/>
|
|
</Modal>
|
|
</PublicLayout>
|
|
);
|
|
}
|
|
|
|
export default AftersalesConfirmPage;
|