refactor(工单管理): 重构设备选择表单并优化UI体验 fix(工单统计): 修复每日完成工单统计不准确的问题 perf(后端): 添加批量导出接口优化大数据量查询性能 chore: 添加react-transition-group依赖支持动画效果 feat(设备管理): 添加设备全量查询接口支持导出功能 style(工单管理): 优化表单布局和样式提升用户体验 refactor(工单字段): 优化字段管理组件处理空值情况 fix(机柜管理): 修复机柜列表查询接口参数问题 docs: 移除不再使用的工单字段管理页面
565 lines
16 KiB
React
565 lines
16 KiB
React
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message, Button, Switch, Tooltip } from 'antd';
|
|
import {
|
|
BarChartOutlined,
|
|
PieChartOutlined,
|
|
RiseOutlined,
|
|
FallOutlined,
|
|
ClockCircleOutlined,
|
|
CheckCircleOutlined,
|
|
ExclamationCircleOutlined,
|
|
ReloadOutlined,
|
|
SyncOutlined,
|
|
} from '@ant-design/icons';
|
|
import axios from 'axios';
|
|
import dayjs from 'dayjs';
|
|
|
|
const { RangePicker } = DatePicker;
|
|
const { Option } = Select;
|
|
|
|
const REFRESH_INTERVAL = 30000;
|
|
|
|
const getStatusColor = status => {
|
|
const colors = {
|
|
pending: 'orange',
|
|
assigned: 'blue',
|
|
in_progress: 'processing',
|
|
completed: 'green',
|
|
closed: 'default',
|
|
};
|
|
return colors[status] || 'default';
|
|
};
|
|
|
|
const getStatusText = status => {
|
|
const texts = {
|
|
pending: '待处理',
|
|
assigned: '已分配',
|
|
in_progress: '处理中',
|
|
completed: '已完成',
|
|
closed: '已关闭',
|
|
};
|
|
return texts[status] || status;
|
|
};
|
|
|
|
const getPriorityColor = priority => {
|
|
const colors = {
|
|
low: 'green',
|
|
medium: 'orange',
|
|
high: 'red',
|
|
urgent: 'magenta',
|
|
};
|
|
return colors[priority] || 'default';
|
|
};
|
|
|
|
const getPriorityText = priority => {
|
|
const texts = {
|
|
low: '低',
|
|
medium: '中',
|
|
high: '高',
|
|
urgent: '紧急',
|
|
};
|
|
return texts[priority] || priority;
|
|
};
|
|
|
|
function TicketStatistics() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
|
const [dateRange, setDateRange] = useState([dayjs().subtract(30, 'days'), dayjs()]);
|
|
const [lastUpdateTime, setLastUpdateTime] = useState(null);
|
|
const [statistics, setStatistics] = useState({
|
|
total: 0,
|
|
pending: 0,
|
|
inProgress: 0,
|
|
completed: 0,
|
|
closed: 0,
|
|
avgProcessingTime: 0,
|
|
byCategory: [],
|
|
byPriority: [],
|
|
byStatus: [],
|
|
byDevice: [],
|
|
trend: [],
|
|
});
|
|
|
|
const timerRef = useRef(null);
|
|
|
|
const fetchStatistics = useCallback(async (isManual = false) => {
|
|
try {
|
|
if (isManual) {
|
|
setLoading(true);
|
|
}
|
|
const params = {
|
|
startDate: dateRange[0].format('YYYY-MM-DD'),
|
|
endDate: dateRange[1].format('YYYY-MM-DD'),
|
|
};
|
|
|
|
const response = await axios.get('/api/tickets/stats', { params });
|
|
setStatistics(response.data);
|
|
setLastUpdateTime(new Date());
|
|
} catch (error) {
|
|
message.error('获取统计数据失败');
|
|
console.error('获取统计数据失败:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [dateRange]);
|
|
|
|
useEffect(() => {
|
|
fetchStatistics();
|
|
}, [fetchStatistics]);
|
|
|
|
useEffect(() => {
|
|
if (autoRefresh) {
|
|
timerRef.current = setInterval(() => {
|
|
fetchStatistics();
|
|
}, REFRESH_INTERVAL);
|
|
} else {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
}
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
}
|
|
};
|
|
}, [autoRefresh, fetchStatistics]);
|
|
|
|
const handleDateChange = useCallback(dates => {
|
|
if (dates) {
|
|
setDateRange(dates);
|
|
}
|
|
}, []);
|
|
|
|
const handleManualRefresh = useCallback(() => {
|
|
fetchStatistics(true);
|
|
}, [fetchStatistics]);
|
|
|
|
const getStatusColor = status => {
|
|
const colors = {
|
|
pending: 'orange',
|
|
assigned: 'blue',
|
|
in_progress: 'processing',
|
|
completed: 'green',
|
|
closed: 'default',
|
|
};
|
|
return colors[status] || 'default';
|
|
};
|
|
|
|
const getStatusText = status => {
|
|
const texts = {
|
|
pending: '待处理',
|
|
assigned: '已分配',
|
|
in_progress: '处理中',
|
|
completed: '已完成',
|
|
closed: '已关闭',
|
|
};
|
|
return texts[status] || status;
|
|
};
|
|
|
|
const getPriorityColor = priority => {
|
|
const colors = {
|
|
low: 'green',
|
|
medium: 'orange',
|
|
high: 'red',
|
|
urgent: 'magenta',
|
|
};
|
|
return colors[priority] || 'default';
|
|
};
|
|
|
|
const getPriorityText = priority => {
|
|
const texts = {
|
|
low: '低',
|
|
medium: '中',
|
|
high: '高',
|
|
urgent: '紧急',
|
|
};
|
|
return texts[priority] || priority;
|
|
};
|
|
|
|
const statusColumns = useMemo(
|
|
() => [
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 120,
|
|
render: status => <Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>,
|
|
},
|
|
{
|
|
title: '工单数量',
|
|
dataIndex: 'count',
|
|
key: 'count',
|
|
width: 120,
|
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
|
},
|
|
{
|
|
title: '占比',
|
|
dataIndex: 'percentage',
|
|
key: 'percentage',
|
|
width: 120,
|
|
render: pct => (
|
|
<span style={{ color: pct > 30 ? '#ff4d4f' : '#52c41a' }}>
|
|
{pct !== undefined && pct !== null ? `${pct.toFixed(1)}%` : '-'}
|
|
</span>
|
|
),
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
const categoryColumns = useMemo(
|
|
() => [
|
|
{
|
|
title: '故障分类',
|
|
dataIndex: 'category',
|
|
key: 'category',
|
|
width: 150,
|
|
},
|
|
{
|
|
title: '工单数量',
|
|
dataIndex: 'count',
|
|
key: 'count',
|
|
width: 120,
|
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
|
},
|
|
{
|
|
title: '占比',
|
|
dataIndex: 'percentage',
|
|
key: 'percentage',
|
|
width: 100,
|
|
render: pct => `${pct !== undefined && pct !== null ? pct.toFixed(1) : 0}%`,
|
|
},
|
|
{
|
|
title: '已完成',
|
|
dataIndex: 'completed',
|
|
key: 'completed',
|
|
width: 100,
|
|
render: count => <Tag color="green">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '平均处理时间(小时)',
|
|
dataIndex: 'avgTime',
|
|
key: 'avgTime',
|
|
width: 150,
|
|
render: time => (time !== undefined && time !== null ? time.toFixed(1) : '-'),
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
const priorityColumns = useMemo(
|
|
() => [
|
|
{
|
|
title: '优先级',
|
|
dataIndex: 'priority',
|
|
key: 'priority',
|
|
width: 100,
|
|
render: priority => (
|
|
<Tag color={getPriorityColor(priority)}>{getPriorityText(priority)}</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '工单数量',
|
|
dataIndex: 'count',
|
|
key: 'count',
|
|
width: 120,
|
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
|
},
|
|
{
|
|
title: '已完成',
|
|
dataIndex: 'completed',
|
|
key: 'completed',
|
|
width: 100,
|
|
render: count => <Tag color="green">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '平均处理时间(小时)',
|
|
dataIndex: 'avgTime',
|
|
key: 'avgTime',
|
|
width: 150,
|
|
render: time => (time !== undefined && time !== null ? time.toFixed(1) : '-'),
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
const deviceColumns = useMemo(
|
|
() => [
|
|
{
|
|
title: '设备名称',
|
|
dataIndex: 'deviceName',
|
|
key: 'deviceName',
|
|
width: 180,
|
|
},
|
|
{
|
|
title: '故障次数',
|
|
dataIndex: 'count',
|
|
key: 'count',
|
|
width: 100,
|
|
render: count => <Tag color="red">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '最后故障时间',
|
|
dataIndex: 'lastFaultTime',
|
|
key: 'lastFaultTime',
|
|
width: 160,
|
|
render: text => (text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'),
|
|
},
|
|
{
|
|
title: '设备类型',
|
|
dataIndex: 'deviceType',
|
|
key: 'deviceType',
|
|
width: 100,
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
const simpleBarData = [
|
|
{ name: '待处理', value: statistics.pending },
|
|
{ name: '处理中', value: statistics.inProgress },
|
|
{ name: '已完成', value: statistics.completed },
|
|
{ name: '已关闭', value: statistics.closed },
|
|
];
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Card
|
|
title="工单统计报表"
|
|
extra={
|
|
<Space>
|
|
<Tooltip title={lastUpdateTime ? `最后更新: ${dayjs(lastUpdateTime).format('HH:mm:ss')}` : '尚未更新'}>
|
|
<span style={{ fontSize: 12, color: '#888', marginRight: 8 }}>
|
|
{lastUpdateTime && `更新于 ${dayjs(lastUpdateTime).format('HH:mm:ss')}`}
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip title="自动刷新(每30秒)">
|
|
<Switch
|
|
checked={autoRefresh}
|
|
onChange={setAutoRefresh}
|
|
checkedChildren={<SyncOutlined spin={autoRefresh} />}
|
|
unCheckedChildren={<SyncOutlined />}
|
|
size="small"
|
|
/>
|
|
</Tooltip>
|
|
<Button icon={<ReloadOutlined />} onClick={handleManualRefresh} size="small">
|
|
刷新
|
|
</Button>
|
|
<RangePicker value={dateRange} onChange={handleDateChange} allowClear={false} />
|
|
</Space>
|
|
}
|
|
>
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#f0f5ff' }}>
|
|
<Statistic
|
|
title="工单总数"
|
|
value={statistics.total}
|
|
prefix={<BarChartOutlined style={{ color: '#1890ff' }} />}
|
|
valueStyle={{ color: '#1890ff' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#fff7e6' }}>
|
|
<Statistic
|
|
title="待处理工单"
|
|
value={statistics.pending}
|
|
prefix={<ExclamationCircleOutlined style={{ color: '#fa8c16' }} />}
|
|
valueStyle={{ color: '#fa8c16' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#e6f7ff' }}>
|
|
<Statistic
|
|
title="已完成工单"
|
|
value={statistics.completed}
|
|
prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
|
valueStyle={{ color: '#52c41a' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#f9f0ff' }}>
|
|
<Statistic
|
|
title="平均处理时长(小时)"
|
|
value={statistics.avgProcessingTime || 0}
|
|
prefix={<ClockCircleOutlined style={{ color: '#722ed1' }} />}
|
|
valueStyle={{ color: '#722ed1' }}
|
|
precision={1}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#fff0f6' }}>
|
|
<Statistic
|
|
title="处理中工单"
|
|
value={statistics.inProgress}
|
|
prefix={<RiseOutlined style={{ color: '#eb2f96' }} />}
|
|
valueStyle={{ color: '#eb2f96' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#f5f5f5' }}>
|
|
<Statistic
|
|
title="已关闭工单"
|
|
value={statistics.closed}
|
|
prefix={<FallOutlined style={{ color: '#8c8c8c' }} />}
|
|
valueStyle={{ color: '#8c8c8c' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#fff1f0' }}>
|
|
<Statistic
|
|
title="完成率"
|
|
value={
|
|
statistics.total > 0
|
|
? ((statistics.completed / statistics.total) * 100).toFixed(1)
|
|
: 0
|
|
}
|
|
suffix="%"
|
|
prefix={<PieChartOutlined style={{ color: '#ff4d4f' }} />}
|
|
valueStyle={{ color: '#ff4d4f' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} md={6}>
|
|
<Card bordered={false} style={{ background: '#f6ffed' }}>
|
|
<Statistic
|
|
title="处理中占比"
|
|
value={
|
|
statistics.total > 0
|
|
? ((statistics.inProgress / statistics.total) * 100).toFixed(1)
|
|
: 0
|
|
}
|
|
suffix="%"
|
|
prefix={<RiseOutlined style={{ color: '#52c41a' }} />}
|
|
valueStyle={{ color: '#52c41a' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24} lg={12}>
|
|
<Card title="按状态分布" loading={loading}>
|
|
<Table
|
|
columns={statusColumns}
|
|
dataSource={statistics.byStatus}
|
|
rowKey="status"
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} lg={12}>
|
|
<Card title="按优先级分布" loading={loading}>
|
|
<Table
|
|
columns={priorityColumns}
|
|
dataSource={statistics.byPriority}
|
|
rowKey="priority"
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24}>
|
|
<Card title="按故障分类统计" loading={loading}>
|
|
<Table
|
|
columns={categoryColumns}
|
|
dataSource={statistics.byCategory}
|
|
rowKey="category"
|
|
pagination={{ pageSize: 10 }}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24}>
|
|
<Card title="故障频发设备排行" loading={loading}>
|
|
<Table
|
|
columns={deviceColumns}
|
|
dataSource={statistics.byDevice}
|
|
rowKey="deviceId"
|
|
pagination={{ pageSize: 10 }}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24}>
|
|
<Card title="工单趋势统计" loading={loading}>
|
|
<Table
|
|
columns={[
|
|
{
|
|
title: '日期',
|
|
dataIndex: 'date',
|
|
key: 'date',
|
|
width: 120,
|
|
render: text => (text ? dayjs(text).format('YYYY-MM-DD') : '-'),
|
|
},
|
|
{
|
|
title: '新建工单',
|
|
dataIndex: 'created',
|
|
key: 'created',
|
|
width: 100,
|
|
render: count => <Tag color="blue">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '已完成',
|
|
dataIndex: 'completed',
|
|
key: 'completed',
|
|
width: 100,
|
|
render: count => <Tag color="green">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '关闭工单',
|
|
dataIndex: 'closed',
|
|
key: 'closed',
|
|
width: 100,
|
|
render: count => <Tag color="default">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '当日处理中',
|
|
dataIndex: 'inProgress',
|
|
key: 'inProgress',
|
|
width: 120,
|
|
render: count => <Tag color="processing">{count}</Tag>,
|
|
},
|
|
{
|
|
title: '新增待处理',
|
|
dataIndex: 'pending',
|
|
key: 'pending',
|
|
width: 120,
|
|
render: count => <Tag color="orange">{count}</Tag>,
|
|
},
|
|
]}
|
|
dataSource={statistics.trend}
|
|
rowKey="date"
|
|
pagination={{ pageSize: 10 }}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default React.memo(TicketStatistics);
|