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:
@@ -0,0 +1,391 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Modal,
|
||||
Tag,
|
||||
Form,
|
||||
Select,
|
||||
Pagination,
|
||||
} from 'antd';
|
||||
import {
|
||||
ToolOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { aftersalesApi, authApi } from '@/services/api';
|
||||
import type {
|
||||
AftersalesOrder,
|
||||
AftersalesWorkOrderStatus,
|
||||
AftersalesServiceType,
|
||||
CreateAftersalesRequest,
|
||||
} from '@/types';
|
||||
|
||||
const WORK_ORDER_STATUS_LABEL: Record<AftersalesWorkOrderStatus, string> = {
|
||||
created: '待处理',
|
||||
pending_confirmation: '待客户确认',
|
||||
closed: '已完成',
|
||||
rejected: '已退回',
|
||||
};
|
||||
|
||||
const WORK_ORDER_STATUS_COLOR: Record<AftersalesWorkOrderStatus, string> = {
|
||||
created: 'default',
|
||||
pending_confirmation: 'processing',
|
||||
closed: 'success',
|
||||
rejected: 'warning',
|
||||
};
|
||||
|
||||
const SERVICE_TYPE_LABEL: Record<AftersalesServiceType, string> = {
|
||||
software: '软件',
|
||||
hardware: '硬件',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
function AftersalesPage() {
|
||||
const navigate = useNavigate();
|
||||
const currentUser = authApi.getCurrentUser();
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const [orders, setOrders] = useState<AftersalesOrder[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [limit, setLimit] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [workOrderStatus, setWorkOrderStatus] = useState<AftersalesWorkOrderStatus | undefined>();
|
||||
const [serviceType, setServiceType] = useState<AftersalesServiceType | undefined>();
|
||||
const [mineOnly, setMineOnly] = useState(false);
|
||||
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createForm] = Form.useForm<CreateAftersalesRequest>();
|
||||
|
||||
const loadOrders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await aftersalesApi.list({
|
||||
page,
|
||||
limit,
|
||||
search: search || undefined,
|
||||
workOrderStatus,
|
||||
serviceType,
|
||||
mine: isAdmin ? mineOnly : undefined,
|
||||
});
|
||||
setOrders(result.data || []);
|
||||
setTotal(result.pagination?.total || 0);
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '加载工单列表失败');
|
||||
setOrders([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadOrders();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, limit, search, workOrderStatus, serviceType, mineOnly]);
|
||||
|
||||
const handleCreate = async (values: CreateAftersalesRequest) => {
|
||||
setCreating(true);
|
||||
try {
|
||||
const order = await aftersalesApi.create(values);
|
||||
message.success(`工单创建成功:${order.serialNumber}`);
|
||||
setCreateModalVisible(false);
|
||||
createForm.resetFields();
|
||||
navigate(`/admin/aftersales/${order.serialNumber}`);
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '创建失败');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (order: AftersalesOrder) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除工单 ${order.serialNumber} 吗?此操作不可恢复!`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await aftersalesApi.delete(order.serialNumber);
|
||||
message.success('删除成功');
|
||||
loadOrders();
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '工单号',
|
||||
dataIndex: 'serialNumber',
|
||||
key: 'serialNumber',
|
||||
width: 180,
|
||||
render: (sn: string) => (
|
||||
<span style={{ fontFamily: 'monospace', color: '#165DFF' }}>{sn}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户公司',
|
||||
dataIndex: 'companyName',
|
||||
key: 'companyName',
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contactName',
|
||||
key: 'contactName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '服务类型',
|
||||
dataIndex: 'serviceType',
|
||||
key: 'serviceType',
|
||||
width: 100,
|
||||
render: (type: AftersalesServiceType) => SERVICE_TYPE_LABEL[type] || type,
|
||||
},
|
||||
{
|
||||
title: '负责技术员',
|
||||
key: 'technician',
|
||||
width: 120,
|
||||
render: (_: any, record: AftersalesOrder) => record.technician?.name || '-',
|
||||
},
|
||||
{
|
||||
title: '工单状态',
|
||||
dataIndex: 'workOrderStatus',
|
||||
key: 'workOrderStatus',
|
||||
width: 130,
|
||||
render: (status: AftersalesWorkOrderStatus, record: AftersalesOrder) => (
|
||||
<Space size={4}>
|
||||
<Tag color={WORK_ORDER_STATUS_COLOR[status]}>
|
||||
{WORK_ORDER_STATUS_LABEL[status]}
|
||||
</Tag>
|
||||
{record.rejectCount > 0 && (
|
||||
<Tag color="orange" style={{ marginLeft: 0 }}>
|
||||
退回 {record.rejectCount} 次
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 170,
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 180,
|
||||
render: (_: any, record: AftersalesOrder) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/admin/aftersales/${record.serialNumber}`)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<ToolOutlined />
|
||||
<span>售后工单</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
>
|
||||
新建工单
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Space style={{ marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<Input.Search
|
||||
placeholder="搜索工单号/公司/联系人"
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
onSearch={(v) => {
|
||||
setPage(1);
|
||||
setSearch(v);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) {
|
||||
setPage(1);
|
||||
setSearch('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder="工单状态"
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
value={workOrderStatus}
|
||||
onChange={(v) => {
|
||||
setPage(1);
|
||||
setWorkOrderStatus(v);
|
||||
}}
|
||||
options={(Object.keys(WORK_ORDER_STATUS_LABEL) as AftersalesWorkOrderStatus[]).map(
|
||||
(k) => ({ value: k, label: WORK_ORDER_STATUS_LABEL[k] })
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="服务类型"
|
||||
allowClear
|
||||
style={{ width: 130 }}
|
||||
value={serviceType}
|
||||
onChange={(v) => {
|
||||
setPage(1);
|
||||
setServiceType(v);
|
||||
}}
|
||||
options={(Object.keys(SERVICE_TYPE_LABEL) as AftersalesServiceType[]).map((k) => ({
|
||||
value: k,
|
||||
label: SERVICE_TYPE_LABEL[k],
|
||||
}))}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
type={mineOnly ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setMineOnly(!mineOnly);
|
||||
}}
|
||||
>
|
||||
{mineOnly ? '查看全部' : '我负责的'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="serialNumber"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={limit}
|
||||
total={total}
|
||||
onChange={(newPage, newLimit) => {
|
||||
setPage(newPage);
|
||||
setLimit(newLimit);
|
||||
}}
|
||||
showSizeChanger
|
||||
showTotal={(t) => `共计 ${t} 条记录`}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="新建售后工单"
|
||||
open={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false);
|
||||
createForm.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={560}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" onFinish={handleCreate}>
|
||||
<Form.Item
|
||||
name="companyName"
|
||||
label="客户公司"
|
||||
rules={[{ required: true, message: '请输入客户公司' }]}
|
||||
>
|
||||
<Input placeholder="例如:浙江北凡科技" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="companyAddress"
|
||||
label="公司位置"
|
||||
rules={[{ required: true, message: '请输入公司位置' }]}
|
||||
>
|
||||
<Input placeholder="例如:杭州市西湖区文三路 100 号" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="contactName"
|
||||
label="联系人"
|
||||
rules={[{ required: true, message: '请输入联系人' }]}
|
||||
>
|
||||
<Input placeholder="联系人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="contactPhone"
|
||||
label="联系电话"
|
||||
rules={[
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{ pattern: /^\d{11}$/, message: '请输入 11 位手机号' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="11 位手机号" maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="serviceType"
|
||||
label="服务类型"
|
||||
rules={[{ required: true, message: '请选择服务类型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
options={(Object.keys(SERVICE_TYPE_LABEL) as AftersalesServiceType[]).map((k) => ({
|
||||
value: k,
|
||||
label: SERVICE_TYPE_LABEL[k],
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="issueDescription"
|
||||
label="问题反馈"
|
||||
rules={[{ required: true, message: '请填写问题反馈' }]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请描述客户反馈的问题" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => setCreateModalVisible(false)}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={creating}>
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AftersalesPage;
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, Input, Button, Spin, Result, message, Modal } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { aftersalesApi } from '@/services/api';
|
||||
import type { AftersalesPublicView, AftersalesServiceType } from '@/types';
|
||||
import PublicLayout, { PublicLogo } from '@/components/PublicLayout';
|
||||
import './styles/PublicQuery.css';
|
||||
import './styles/AftersalesConfirm.css';
|
||||
|
||||
const SERVICE_TYPE_LABEL: Record<AftersalesServiceType, string> = {
|
||||
software: '软件',
|
||||
hardware: '硬件',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
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 [phoneLast4, setPhoneLast4] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
||||
|
||||
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 validatePhoneInput = () => {
|
||||
if (!/^\d{4}$/.test(phoneLast4)) {
|
||||
message.error('请输入 4 位手机号');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleAuthorize = async () => {
|
||||
if (!validatePhoneInput()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
||||
phoneLast4,
|
||||
action: 'authorize',
|
||||
});
|
||||
setOrder(updated);
|
||||
message.success('感谢您的确认,工单已完成');
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '提交失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!validatePhoneInput()) return;
|
||||
setShowRejectDialog(true);
|
||||
};
|
||||
|
||||
const confirmReject = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await aftersalesApi.customerConfirm(serialNumber, {
|
||||
phoneLast4,
|
||||
action: 'reject',
|
||||
rejectReason: rejectReason.trim() || undefined,
|
||||
});
|
||||
setOrder(updated);
|
||||
setShowRejectDialog(false);
|
||||
message.success('已通知技术员重新处理');
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '提交失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{isPending && (
|
||||
<>
|
||||
<div className="aftersales-phone-verify">
|
||||
<p className="aftersales-phone-tip">请输入联系人手机号后 4 位确认身份</p>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder="手机号后 4 位"
|
||||
maxLength={4}
|
||||
value={phoneLast4}
|
||||
onChange={(e) => setPhoneLast4(e.target.value.replace(/\D/g, ''))}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
<div className="aftersales-actions">
|
||||
<Button
|
||||
size="large"
|
||||
danger
|
||||
block
|
||||
loading={submitting}
|
||||
onClick={handleReject}
|
||||
>
|
||||
未授权
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
onClick={handleAuthorize}
|
||||
>
|
||||
已授权
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<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;
|
||||
@@ -0,0 +1,452 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Modal,
|
||||
Spin,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
QrcodeOutlined,
|
||||
SaveOutlined,
|
||||
SendOutlined,
|
||||
StopOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { aftersalesApi, authApi } from '@/services/api';
|
||||
import type {
|
||||
AftersalesOrder,
|
||||
AftersalesServiceType,
|
||||
AftersalesWorkOrderStatus,
|
||||
UpdateAftersalesRequest,
|
||||
} from '@/types';
|
||||
|
||||
const SERVICE_TYPE_LABEL: Record<AftersalesServiceType, string> = {
|
||||
software: '软件',
|
||||
hardware: '硬件',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
const WORK_ORDER_STATUS_LABEL: Record<AftersalesWorkOrderStatus, string> = {
|
||||
created: '待处理',
|
||||
pending_confirmation: '待客户确认',
|
||||
closed: '已完成',
|
||||
rejected: '已退回',
|
||||
};
|
||||
|
||||
const WORK_ORDER_STATUS_COLOR: Record<AftersalesWorkOrderStatus, string> = {
|
||||
created: 'default',
|
||||
pending_confirmation: 'processing',
|
||||
closed: 'success',
|
||||
rejected: 'warning',
|
||||
};
|
||||
|
||||
function statusStepIndex(status: AftersalesWorkOrderStatus): number {
|
||||
switch (status) {
|
||||
case 'created':
|
||||
return 0;
|
||||
case 'pending_confirmation':
|
||||
return 1;
|
||||
case 'closed':
|
||||
case 'rejected':
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
function AftersalesDetailPage() {
|
||||
const { serialNumber = '' } = useParams<{ serialNumber: string }>();
|
||||
const navigate = useNavigate();
|
||||
const currentUser = authApi.getCurrentUser();
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const [order, setOrder] = useState<AftersalesOrder | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [qrModalVisible, setQrModalVisible] = useState(false);
|
||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
||||
const [qrUrl, setQrUrl] = useState('');
|
||||
|
||||
const [reassignModalVisible, setReassignModalVisible] = useState(false);
|
||||
const [reassignTechnicianId, setReassignTechnicianId] = useState<number | undefined>();
|
||||
|
||||
const loadOrder = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await aftersalesApi.get(serialNumber);
|
||||
setOrder(data);
|
||||
form.setFieldsValue({
|
||||
companyAddress: data.companyAddress,
|
||||
contactName: data.contactName,
|
||||
contactPhone: data.contactPhone,
|
||||
serviceType: data.serviceType,
|
||||
issueDescription: data.issueDescription,
|
||||
resolutionNote: data.resolutionNote,
|
||||
});
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '加载工单失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (serialNumber) loadOrder();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serialNumber]);
|
||||
|
||||
const handleSave = async (values: UpdateAftersalesRequest) => {
|
||||
if (!order) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const updated = await aftersalesApi.update(order.serialNumber, values);
|
||||
setOrder(updated);
|
||||
message.success('保存成功');
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitForConfirmation = async () => {
|
||||
if (!order) return;
|
||||
const resolutionNote = form.getFieldValue('resolutionNote')?.trim();
|
||||
if (!resolutionNote) {
|
||||
message.error('请先填写处理结果');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认提交',
|
||||
content: '提交后工单将进入"待客户确认"状态,客户扫码确认后才能关闭工单。',
|
||||
okText: '提交',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await aftersalesApi.submit(order.serialNumber, resolutionNote);
|
||||
setOrder(updated);
|
||||
message.success('已提交客户确认');
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '提交失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateQrCode = async () => {
|
||||
if (!order) return;
|
||||
try {
|
||||
const result = await aftersalesApi.generateQrCode(order.serialNumber);
|
||||
const data = result.qrCodeData.startsWith('data:')
|
||||
? result.qrCodeData
|
||||
: `data:image/png;base64,${result.qrCodeData}`;
|
||||
setQrCodeDataUrl(data);
|
||||
setQrUrl(result.queryUrl);
|
||||
setQrModalVisible(true);
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '生成二维码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadQR = () => {
|
||||
if (!order || !qrCodeDataUrl) return;
|
||||
const link = document.createElement('a');
|
||||
link.download = `${order.serialNumber}.png`;
|
||||
link.href = qrCodeDataUrl;
|
||||
link.click();
|
||||
};
|
||||
|
||||
const handleForceClose = () => {
|
||||
if (!order) return;
|
||||
Modal.confirm({
|
||||
title: '强制关闭工单',
|
||||
content: '强制关闭后工单状态将变为"已完成",且不可恢复。',
|
||||
okText: '强制关闭',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const updated = await aftersalesApi.forceClose(order.serialNumber);
|
||||
setOrder(updated);
|
||||
message.success('工单已强制关闭');
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '强制关闭失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleReassign = async () => {
|
||||
if (!order || !reassignTechnicianId) {
|
||||
message.error('请输入新的技术员 ID');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = await aftersalesApi.reassign(order.serialNumber, reassignTechnicianId);
|
||||
setOrder(updated);
|
||||
message.success('重新分配成功');
|
||||
setReassignModalVisible(false);
|
||||
setReassignTechnicianId(undefined);
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || err.message || '重新分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '80px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isClosed = order.workOrderStatus === 'closed';
|
||||
const isPendingCustomer = order.workOrderStatus === 'pending_confirmation';
|
||||
const canSubmit =
|
||||
!isClosed &&
|
||||
!isPendingCustomer &&
|
||||
(isAdmin || order.technicianId === currentUser?.id);
|
||||
const canEdit = !isClosed && (isAdmin || order.technicianId === currentUser?.id);
|
||||
|
||||
const stepStatus =
|
||||
order.workOrderStatus === 'rejected'
|
||||
? 'error'
|
||||
: isClosed
|
||||
? 'finish'
|
||||
: 'process';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/admin/aftersales')}
|
||||
/>
|
||||
<span style={{ fontFamily: 'monospace', color: '#165DFF' }}>
|
||||
{order.serialNumber}
|
||||
</span>
|
||||
<Tag color={WORK_ORDER_STATUS_COLOR[order.workOrderStatus]}>
|
||||
{WORK_ORDER_STATUS_LABEL[order.workOrderStatus]}
|
||||
</Tag>
|
||||
{order.rejectCount > 0 && <Tag color="orange">退回 {order.rejectCount} 次</Tag>}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<QrcodeOutlined />} onClick={handleGenerateQrCode}>
|
||||
生成二维码
|
||||
</Button>
|
||||
{isAdmin && !isClosed && (
|
||||
<>
|
||||
<Button icon={<UserSwitchOutlined />} onClick={() => setReassignModalVisible(true)}>
|
||||
重新分配
|
||||
</Button>
|
||||
<Button danger icon={<StopOutlined />} onClick={handleForceClose}>
|
||||
强制关闭
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Steps
|
||||
current={statusStepIndex(order.workOrderStatus)}
|
||||
status={stepStatus}
|
||||
items={[
|
||||
{ title: '工单创建', description: new Date(order.createdAt).toLocaleString('zh-CN') },
|
||||
{
|
||||
title: '待客户确认',
|
||||
description: order.scannedAt
|
||||
? `客户扫码 ${new Date(order.scannedAt).toLocaleString('zh-CN')}`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
title: order.workOrderStatus === 'rejected' ? '客户退回' : '工单完成',
|
||||
description: order.confirmedAt
|
||||
? new Date(order.confirmedAt).toLocaleString('zh-CN')
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Descriptions column={2} size="small" bordered style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="客户公司">{order.companyName}</Descriptions.Item>
|
||||
<Descriptions.Item label="负责技术员">
|
||||
{order.technician?.name || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建人">{order.creator?.name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="授权状态">
|
||||
{order.authorizationStatus === 'authorized'
|
||||
? '已授权'
|
||||
: order.authorizationStatus === 'unauthorized'
|
||||
? '未授权'
|
||||
: '待确认'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Form form={form} layout="vertical" onFinish={handleSave} disabled={!canEdit}>
|
||||
<Form.Item
|
||||
name="companyAddress"
|
||||
label="公司位置"
|
||||
rules={[{ required: true, message: '请输入公司位置' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space style={{ display: 'flex' }} size={16}>
|
||||
<Form.Item
|
||||
name="contactName"
|
||||
label="联系人"
|
||||
rules={[{ required: true, message: '请输入联系人' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="contactPhone"
|
||||
label="联系电话"
|
||||
rules={[
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{ pattern: /^\d{11}$/, message: '请输入 11 位手机号' },
|
||||
]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="serviceType"
|
||||
label="服务类型"
|
||||
rules={[{ required: true, message: '请选择服务类型' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Select
|
||||
options={(Object.keys(SERVICE_TYPE_LABEL) as AftersalesServiceType[]).map((k) => ({
|
||||
value: k,
|
||||
label: SERVICE_TYPE_LABEL[k],
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item
|
||||
name="issueDescription"
|
||||
label="问题反馈"
|
||||
rules={[{ required: true, message: '请填写问题反馈' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="resolutionNote"
|
||||
label="处理结果"
|
||||
tooltip="维修完成后填写,提交客户确认后客户可在公开页看到此内容"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请描述维修过程和最终处理结果" />
|
||||
</Form.Item>
|
||||
|
||||
{canEdit && (
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
|
||||
保存
|
||||
</Button>
|
||||
{canSubmit && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={submitting}
|
||||
onClick={handleSubmitForConfirmation}
|
||||
>
|
||||
提交客户确认
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="售后工单二维码"
|
||||
open={qrModalVisible}
|
||||
onCancel={() => setQrModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="download" onClick={handleDownloadQR}>
|
||||
下载二维码
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={() => setQrModalVisible(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
{qrCodeDataUrl && (
|
||||
<>
|
||||
<img src={qrCodeDataUrl} alt="QR Code" style={{ width: 220, height: 220 }} />
|
||||
<p
|
||||
style={{
|
||||
marginTop: 12,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#165DFF',
|
||||
}}
|
||||
>
|
||||
{order.serialNumber}
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: '#888', wordBreak: 'break-all' }}>{qrUrl}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="重新分配技术员"
|
||||
open={reassignModalVisible}
|
||||
onCancel={() => setReassignModalVisible(false)}
|
||||
onOk={handleReassign}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="新技术员 ID" required>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入技术员的用户 ID"
|
||||
value={reassignTechnicianId}
|
||||
onChange={(e) =>
|
||||
setReassignTechnicianId(e.target.value ? Number(e.target.value) : undefined)
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<p style={{ color: '#888', fontSize: 12 }}>
|
||||
后续会在用户管理页提供技术员列表选择,目前先输入 ID。
|
||||
</p>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AftersalesDetailPage;
|
||||
+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;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.aftersales-confirm-card {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.aftersales-title {
|
||||
font-size: clamp(1.4rem, 4vw, 1.75rem);
|
||||
font-weight: bold;
|
||||
color: #165DFF;
|
||||
text-align: center;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.aftersales-serial {
|
||||
text-align: center;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: #4B5563;
|
||||
margin: 0 0 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.aftersales-details .detail-item-block {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aftersales-details .value-block {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aftersales-phone-verify {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.aftersales-phone-tip {
|
||||
color: #4B5563;
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aftersales-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.aftersales-actions > button {
|
||||
flex: 1;
|
||||
}
|
||||
Reference in New Issue
Block a user