Add aftersales work order frontend pages
- Public scan-to-confirm page (/aftersales/:sn) with phone last-4 verification - Admin list + detail pages with state machine, QR generation, reassign, force-close - PublicLayout extracted from PublicQuery so both pages share logo + 备案 chrome - PublicQuery auto-redirects scanned zjbf-sh-* serials to the aftersales page - AdminLayout: new 售后工单 menu entry Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-42
@@ -1,11 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Input, Button, Card, message, Spin, Result, Tag } from 'antd';
|
||||
import { QrcodeOutlined, SearchOutlined, ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
QrcodeOutlined,
|
||||
SearchOutlined,
|
||||
ArrowLeftOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { employeeSerialApi } from '@/services/api';
|
||||
import type { Serial } from '@/types';
|
||||
import PublicLayout, { PublicLogo } from '@/components/PublicLayout';
|
||||
import './styles/PublicQuery.css';
|
||||
import logo from '@/assets/img/logo.png?url';
|
||||
import beian from '@/assets/img/beian.png?url';
|
||||
|
||||
interface EmployeeSerialResult {
|
||||
serialNumber: string;
|
||||
@@ -16,7 +22,10 @@ interface EmployeeSerialResult {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const AFTERSALES_PREFIX = 'zjbf-sh-';
|
||||
|
||||
function PublicQueryPage() {
|
||||
const navigate = useNavigate();
|
||||
const [serialNumber, setSerialNumber] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<Serial | EmployeeSerialResult | null>(null);
|
||||
@@ -24,7 +33,14 @@ function PublicQueryPage() {
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
const [serialType, setSerialType] = useState<'company' | 'employee'>('company');
|
||||
|
||||
const isAftersalesSerial = (sn: string) => sn.toLowerCase().startsWith(AFTERSALES_PREFIX);
|
||||
|
||||
const performQuery = async (serialToQuery: string) => {
|
||||
if (isAftersalesSerial(serialToQuery)) {
|
||||
navigate(`/aftersales/${serialToQuery.toLowerCase()}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
@@ -50,10 +66,15 @@ function PublicQueryPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const serialFromUrl = urlParams.get('serial');
|
||||
if (serialFromUrl) {
|
||||
if (isAftersalesSerial(serialFromUrl)) {
|
||||
navigate(`/aftersales/${serialFromUrl.toLowerCase()}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
setSerialNumber(serialFromUrl);
|
||||
setShowResult(true);
|
||||
performQuery(serialFromUrl);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleQuery = async () => {
|
||||
@@ -73,13 +94,11 @@ function PublicQueryPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="public-query-container">
|
||||
<PublicLayout>
|
||||
{!showResult ? (
|
||||
<Card className="query-card" bordered={false}>
|
||||
<div className="query-header">
|
||||
<div className="query-logo">
|
||||
<img src={logo} alt="Logo" style={{ height: '24px' }} />
|
||||
</div>
|
||||
<PublicLogo />
|
||||
<h1 className="query-title">
|
||||
<QrcodeOutlined /> 授权查询
|
||||
</h1>
|
||||
@@ -109,9 +128,7 @@ function PublicQueryPage() {
|
||||
) : (
|
||||
<Card className={`result-card show`} bordered={false}>
|
||||
<div className="result-header">
|
||||
<div className="query-logo">
|
||||
<img src={logo} alt="Logo" style={{ height: '24px' }} />
|
||||
</div>
|
||||
<PublicLogo />
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
@@ -123,20 +140,21 @@ function PublicQueryPage() {
|
||||
{(result as any).isActive === false || (result as any).status === 'inactive' ? (
|
||||
<Result
|
||||
icon={<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '72px' }} />}
|
||||
title={serialType === 'employee' ? "员工身份已吊销" : "授权已吊销"}
|
||||
subTitle={serialType === 'employee'
|
||||
? `序列号验证通过,但已被吊销。企业:${(result as EmployeeSerialResult).companyName}`
|
||||
: `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}`
|
||||
title={serialType === 'employee' ? '员工身份已吊销' : '授权已吊销'}
|
||||
subTitle={
|
||||
serialType === 'employee'
|
||||
? `序列号验证通过,但已被吊销。企业:${(result as EmployeeSerialResult).companyName}`
|
||||
: `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Result
|
||||
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '72px' }} />}
|
||||
title={serialType === 'employee' ? "员工身份有效" : "授权有效"}
|
||||
title={serialType === 'employee' ? '员工身份有效' : '授权有效'}
|
||||
subTitle="您的序列号已验证通过"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
<div className="result-details">
|
||||
<div className="detail-item">
|
||||
<span className="label">序列号</span>
|
||||
@@ -161,14 +179,24 @@ function PublicQueryPage() {
|
||||
{serialType !== 'employee' && (result as Serial).validUntil && (
|
||||
<div className="detail-item">
|
||||
<span className="label">有效期至</span>
|
||||
<span className="value">{new Date((result as Serial).validUntil).toLocaleString('zh-CN')}</span>
|
||||
<span className="value">
|
||||
{new Date((result as Serial).validUntil).toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-item">
|
||||
<span className="label">授权状态</span>
|
||||
<span className="value status">
|
||||
<Tag color={(result as any).isActive === false || (result as any).status === 'inactive' ? 'red' : 'green'}>
|
||||
{(result as any).isActive === false || (result as any).status === 'inactive' ? '已吊销' : '有效'}
|
||||
<Tag
|
||||
color={
|
||||
(result as any).isActive === false || (result as any).status === 'inactive'
|
||||
? 'red'
|
||||
: 'green'
|
||||
}
|
||||
>
|
||||
{(result as any).isActive === false || (result as any).status === 'inactive'
|
||||
? '已吊销'
|
||||
: '有效'}
|
||||
</Tag>
|
||||
</span>
|
||||
</div>
|
||||
@@ -184,33 +212,13 @@ function PublicQueryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
<Button size="large" block icon={<ArrowLeftOutlined />} onClick={handleReset}>
|
||||
重新查询
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="copyright">
|
||||
<p>
|
||||
Copyright © 2026 浙江贝凡网络科技有限公司. All Rights Reserved. |
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
|
||||
浙ICP备2025170226号-4
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=33011002018371" target="_blank" rel="noopener noreferrer">
|
||||
<img src={beian} alt="备案图标" style={{ height: '20px', marginRight: '5px', verticalAlign: 'middle' }} />
|
||||
浙公网安备33011002018371号
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicQueryPage;
|
||||
export default PublicQueryPage;
|
||||
|
||||
Reference in New Issue
Block a user