6fef517556
- 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>
225 lines
7.8 KiB
TypeScript
225 lines
7.8 KiB
TypeScript
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 { 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';
|
|
|
|
interface EmployeeSerialResult {
|
|
serialNumber: string;
|
|
companyName: string;
|
|
department: string;
|
|
employeeName: string;
|
|
isActive: boolean;
|
|
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);
|
|
const [error, setError] = useState<string | null>(null);
|
|
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);
|
|
|
|
try {
|
|
const response = await employeeSerialApi.queryAll(serialToQuery);
|
|
if (response.type === 'employee') {
|
|
setSerialType('employee');
|
|
setResult(response.data as EmployeeSerialResult);
|
|
} else {
|
|
setSerialType('company');
|
|
setResult(response.data as Serial);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message || '查询失败');
|
|
setResult(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
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 () => {
|
|
if (!serialNumber.trim()) {
|
|
message.error('请输入授权序列号');
|
|
return;
|
|
}
|
|
setShowResult(true);
|
|
performQuery(serialNumber.trim());
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setShowResult(false);
|
|
setSerialNumber('');
|
|
setResult(null);
|
|
setError(null);
|
|
};
|
|
|
|
return (
|
|
<PublicLayout>
|
|
{!showResult ? (
|
|
<Card className="query-card" bordered={false}>
|
|
<div className="query-header">
|
|
<PublicLogo />
|
|
<h1 className="query-title">
|
|
<QrcodeOutlined /> 授权查询
|
|
</h1>
|
|
<p className="query-subtitle">请输入您的授权序列号进行查询</p>
|
|
</div>
|
|
|
|
<div className="query-form">
|
|
<Input
|
|
size="large"
|
|
placeholder="请输入或扫描您的授权序列号"
|
|
value={serialNumber}
|
|
onChange={(e) => setSerialNumber(e.target.value)}
|
|
onPressEnter={handleQuery}
|
|
prefix={<SearchOutlined />}
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
size="large"
|
|
block
|
|
icon={<SearchOutlined />}
|
|
onClick={handleQuery}
|
|
>
|
|
立即查询
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<Card className={`result-card show`} bordered={false}>
|
|
<div className="result-header">
|
|
<PublicLogo />
|
|
</div>
|
|
{loading ? (
|
|
<div className="loading-container">
|
|
<Spin size="large" />
|
|
<p>正在查询授权信息...</p>
|
|
</div>
|
|
) : result ? (
|
|
<div className="success-container">
|
|
{(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}`
|
|
}
|
|
/>
|
|
) : (
|
|
<Result
|
|
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '72px' }} />}
|
|
title={serialType === 'employee' ? '员工身份有效' : '授权有效'}
|
|
subTitle="您的序列号已验证通过"
|
|
/>
|
|
)}
|
|
|
|
<div className="result-details">
|
|
<div className="detail-item">
|
|
<span className="label">序列号</span>
|
|
<span className="value serial">{result.serialNumber}</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="label">企业名称</span>
|
|
<span className="value">{result.companyName}</span>
|
|
</div>
|
|
{serialType === 'employee' && (result as EmployeeSerialResult).department && (
|
|
<>
|
|
<div className="detail-item">
|
|
<span className="label">职位</span>
|
|
<span className="value">{(result as EmployeeSerialResult).department}</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="label">员工姓名</span>
|
|
<span className="value">{(result as EmployeeSerialResult).employeeName}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
{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>
|
|
</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>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="error-container">
|
|
<Result
|
|
icon={<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '72px' }} />}
|
|
title="无效序列号"
|
|
subTitle={error}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<Button size="large" block icon={<ArrowLeftOutlined />} onClick={handleReset}>
|
|
重新查询
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
</PublicLayout>
|
|
);
|
|
}
|
|
|
|
export default PublicQueryPage;
|