Files
d8d-vite-starter/src/client/admin/pages/pages_work_orders.tsx
D8D Developer b9a3c991d0 update
2025-06-27 01:56:30 +00:00

904 lines
29 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Button, Table, Space, Modal, Form, Input, Select, message, List, Avatar, Progress, Tag, Timeline, DatePicker, Switch, Dropdown, Menu } from 'antd';
import type { MenuProps } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { WorkOrderAPI } from '../api/work_orders';
import { DeviceInstanceAPI } from '../api/device_instance';
import { Uploader } from '../components/components_uploader';
import { WorkOrderPriority, WorkOrderStatus } from '@/share/monitorTypes';
import type { WorkOrder, WorkOrderSettings, DeadlineInfo } from '@/share/monitorTypes';
const { Column } = Table;
const { Option } = Select;
const { TextArea } = Input;
export function WorkOrdersPage() {
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
const [settings, setSettings] = useState<WorkOrderSettings>();
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [currentOrder, setCurrentOrder] = useState<Partial<WorkOrder>>();
const [categories, setCategories] = useState<string[]>([]);
const [devices, setDevices] = useState<any[]>([]);
const [attachments, setAttachments] = useState<any[]>([]);
const [comments, setComments] = useState<any[]>([]);
const [commentContent, setCommentContent] = useState('');
const [form] = Form.useForm();
const [historyVisible, setHistoryVisible] = useState(false);
const [statusHistory, setStatusHistory] = useState<any[]>([]);
const [deadlineInfo, setDeadlineInfo] = useState<DeadlineInfo>();
const [autoDispatchVisible, setAutoDispatchVisible] = useState(false);
const [autoDispatchForm] = Form.useForm();
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [currentDetail, setCurrentDetail] = useState('');
useEffect(() => {
// 开发环境下生成模拟数据
if (process.env.NODE_ENV === 'development') {
const mockOrders = [
{
id: 'mock-1',
title: '设备网络故障',
order_no: `WO-${dayjs().format('YYYYMMDD')}-1001`,
device_name: '网络交换机1',
problem_desc: '设备无法连接网络',
priority: WorkOrderPriority.URGENT,
creator_id: 'system',
creator_name: '系统管理员',
deadline: dayjs().add(1, 'day').toISOString(),
created_at: dayjs().toISOString(),
updated_at: dayjs().toISOString(),
problem_type: '网络',
status: WorkOrderStatus.PENDING,
feedback: '',
attachments: []
},
{
id: 'mock-2',
title: '服务器硬件故障',
order_no: `WO-${dayjs().format('YYYYMMDD')}-1002`,
device_name: '服务器A',
problem_desc: '硬盘故障需要更换',
priority: WorkOrderPriority.IMPORTANT,
creator_id: 'system',
creator_name: '系统管理员',
deadline: dayjs().add(2, 'day').toISOString(),
created_at: dayjs().toISOString(),
updated_at: dayjs().toISOString(),
problem_type: '硬件',
status: WorkOrderStatus.PROCESSING,
feedback: '已订购新硬盘',
attachments: []
},
{
id: 'mock-3',
title: '软件系统升级',
order_no: `WO-${dayjs().format('YYYYMMDD')}-1003`,
device_name: '办公电脑',
problem_desc: '需要升级到最新版本',
priority: WorkOrderPriority.NORMAL,
creator_id: 'system',
creator_name: '系统管理员',
deadline: dayjs().add(3, 'day').toISOString(),
created_at: dayjs().toISOString(),
updated_at: dayjs().toISOString(),
problem_type: '软件',
status: WorkOrderStatus.CLOSED,
feedback: '已完成升级',
attachments: []
},
{
id: 'mock-4',
title: '打印机维护',
order_no: `WO-${dayjs().format('YYYYMMDD')}-1004`,
device_name: '办公室打印机',
problem_desc: '定期维护保养',
priority: WorkOrderPriority.NORMAL,
creator_id: 'system',
creator_name: '系统管理员',
deadline: dayjs().add(4, 'day').toISOString(),
created_at: dayjs().toISOString(),
updated_at: dayjs().toISOString(),
problem_type: '其他',
status: WorkOrderStatus.PENDING,
feedback: '',
attachments: []
},
{
id: 'mock-5',
title: '数据库优化',
order_no: `WO-${dayjs().format('YYYYMMDD')}-1005`,
device_name: '数据库服务器',
problem_desc: '查询性能优化',
priority: WorkOrderPriority.IMPORTANT,
creator_id: 'system',
creator_name: '系统管理员',
deadline: dayjs().add(5, 'day').toISOString(),
created_at: dayjs().toISOString(),
updated_at: dayjs().toISOString(),
problem_type: '软件',
status: WorkOrderStatus.PROCESSING,
feedback: '正在优化索引',
attachments: []
}
];
setWorkOrders(mockOrders);
} else {
fetchData();
}
fetchSettings();
fetchCategories();
fetchDevices();
}, []);
const [searchParams, setSearchParams] = useState({
status: undefined,
problemType: undefined,
keyword: undefined,
startDate: undefined,
endDate: undefined
});
const fetchData = async () => {
setLoading(true);
try {
const result = await WorkOrderAPI.getList(searchParams);
setWorkOrders(result.data);
} catch (error) {
message.error('获取工单列表失败');
} finally {
setLoading(false);
}
};
const handleSearch = (values: any) => {
setSearchParams({
status: values.status,
problemType: values.problemType,
keyword: values.keyword,
startDate: values.dateRange?.[0]?.toISOString(),
endDate: values.dateRange?.[1]?.toISOString()
});
};
const handleReset = () => {
setSearchParams({
status: undefined,
problemType: undefined,
keyword: undefined,
startDate: undefined,
endDate: undefined
});
};
const fetchSettings = async () => {
try {
const result = await WorkOrderAPI.getSettings();
setSettings(result.data);
} catch (error) {
message.error('获取工单设置失败');
}
};
const fetchCategories = async () => {
try {
const result = await WorkOrderAPI.getCategories();
setCategories(result.data);
} catch (error) {
message.error('获取分类列表失败');
}
};
const fetchDevices = async () => {
try {
const result = await DeviceInstanceAPI.getDeviceInstances();
setDevices(result.data);
} catch (error) {
message.error('获取设备列表失败');
}
};
const fetchComments = async (id: string) => {
try {
const result = await WorkOrderAPI.getComments(id);
setComments(result.data);
} catch (error) {
message.error('获取评论失败');
}
};
const fetchStatusHistory = async (id: string) => {
try {
const result = await WorkOrderAPI.getStatusHistory(id);
setStatusHistory(result.data);
} catch (error) {
message.error('获取状态历史失败');
}
};
const checkDeadline = async (order: WorkOrder) => {
if (!order.deadline) return;
try {
const result = await WorkOrderAPI.getDeadline(order.id);
const { remaining_hours, is_overdue } = result.data;
let color = 'green';
let text = '进行中';
let progress = 100;
if (is_overdue) {
color = 'red';
text = '已超时';
progress = 0;
} else if (remaining_hours < 24) {
color = 'orange';
text = `即将到期 (剩余${remaining_hours}小时)`;
progress = Math.max(10, Math.min(90, remaining_hours * 4));
}
setDeadlineInfo({
color,
text,
progress: Number(progress),
remainingTime: `${remaining_hours}小时`,
isOverdue: remaining_hours < 0,
});
} catch (error) {
message.error('获取时限信息失败');
}
};
const handleCreate = () => {
setCurrentOrder({});
setModalVisible(true);
};
const handleEdit = async (record: WorkOrder) => {
setCurrentOrder(record);
form.setFieldsValue(record);
setModalVisible(true);
checkDeadline(record);
if (record.id) {
await fetchComments(record.id);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (currentOrder?.id) {
await WorkOrderAPI.update(currentOrder.id, values);
message.success('更新工单成功');
} else {
await WorkOrderAPI.create(values);
message.success('创建工单成功');
}
setModalVisible(false);
fetchData();
} catch (error) {
message.error('操作失败');
}
};
const handleStatusChange = async (id: string, status: string) => {
Modal.confirm({
title: '确认状态变更',
content: (
<Form form={form}>
<Form.Item name="comment" label="变更备注" rules={[{required: true}]}>
<Input.TextArea placeholder="请输入状态变更原因" />
</Form.Item>
</Form>
),
onOk: async (close) => {
try {
const values = await form.validateFields();
await WorkOrderAPI.changeStatus(
id,
status,
'current_user', // TODO: 替换为实际用户
values.comment
);
message.success('状态更新成功');
fetchData();
close();
} catch (error) {
message.error('状态更新失败');
}
}
});
};
const renderStatusActions = (record: WorkOrder) => {
const statusOptions = settings?.statusOptions || [];
const currentStatusIndex = statusOptions.indexOf(record.status);
const nextStatus = statusOptions[currentStatusIndex + 1];
const prevStatus = statusOptions[currentStatusIndex - 1];
return (
<Space>
{prevStatus && (
<Button
size="small"
onClick={() => handleStatusChange(record.id, prevStatus)}
>
退{prevStatus}
</Button>
)}
{nextStatus && (
<Button
type="primary"
size="small"
onClick={() => handleStatusChange(record.id, nextStatus)}
>
{nextStatus}
</Button>
)}
{!nextStatus && !prevStatus && (
<span></span>
)}
</Space>
);
};
const handleShowHistory = async (id: string) => {
await fetchStatusHistory(id);
setHistoryVisible(true);
};
const handleAssign = async (id: string, assignee: string) => {
try {
await WorkOrderAPI.assign(id, assignee);
message.success('分配成功');
fetchData();
} catch (error) {
message.error('分配失败');
}
};
const handleUploadSuccess = (fileUrl: string, fileInfo: any) => {
if (currentOrder?.id) {
setAttachments(prev => [...prev, {
id: fileInfo.id,
url: fileUrl,
name: fileInfo.original_filename
}]);
message.success('附件上传成功');
}
};
const handleAddComment = async () => {
if (!currentOrder?.id) {
message.error('请先保存工单');
return;
}
if (!commentContent.trim()) {
message.error('评论内容不能为空');
return;
}
if (commentContent.length > 500) {
message.error('评论内容不能超过500字');
return;
}
// 简单敏感词过滤
const bannedWords = ['敏感词1', '敏感词2', '敏感词3'];
if (bannedWords.some(word => commentContent.includes(word))) {
message.error('评论包含不允许的内容');
return;
}
try {
await WorkOrderAPI.addComment(currentOrder.id, commentContent);
setCommentContent('');
await fetchComments(currentOrder.id);
message.success('评论添加成功');
} catch (error) {
message.error('评论添加失败');
}
};
const handleAccept = async (id: string) => {
try {
await WorkOrderAPI.changeStatus(id, '处理中', 'current_user', '工单已受理');
message.success('工单受理成功');
fetchData();
} catch (error) {
message.error('受理失败');
}
};
const handleReassign = async (id: string) => {
Modal.confirm({
title: '改派工单',
content: (
<Select placeholder="选择新的处理人" style={{ width: '100%' }}>
<Option value="user1">1</Option>
<Option value="user2">2</Option>
<Option value="user3">3</Option>
</Select>
),
onOk: async (close) => {
try {
await WorkOrderAPI.assign(id, 'new_assignee'); // TODO: 替换为实际选择的值
message.success('工单改派成功');
fetchData();
close();
} catch (error) {
message.error('改派失败');
}
}
});
};
const handleClose = async (id: string) => {
Modal.confirm({
title: '关闭工单',
content: (
<Form form={form}>
<Form.Item name="feedback" label="处理结果" rules={[{required: true}]}>
<TextArea placeholder="请输入处理结果反馈" />
</Form.Item>
</Form>
),
onOk: async (close) => {
try {
const values = await form.validateFields();
await WorkOrderAPI.changeStatus(
id,
'已关闭',
'current_user',
values.feedback
);
message.success('工单已关闭');
fetchData();
close();
} catch (error) {
message.error('关闭失败');
}
}
});
};
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Space>
<Button type="primary" onClick={handleCreate}>
</Button>
<Button type="primary" onClick={() => setAutoDispatchVisible(true)}>
</Button>
</Space>
<Space>
<Button
type="primary"
onClick={async () => {
try {
const data = await WorkOrderAPI.exportList(searchParams);
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `工单列表_${dayjs().format('YYYYMMDD')}.xlsx`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('导出成功');
} catch (error) {
message.error('导出失败');
}
}}
>
</Button>
</Space>
</div>
<Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
<Form.Item name="status" label="工单状态">
<Select style={{ width: 120 }} allowClear>
{Object.values(WorkOrderStatus).map(status => (
<Option key={status} value={status}>{status}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="problemType" label="问题分类">
<Select style={{ width: 120 }} allowClear>
{categories.map(category => (
<Option key={category} value={category}>{category}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="keyword" label="关键字">
<Input placeholder="请输入关键字" />
</Form.Item>
<Form.Item name="dateRange" label="时间范围">
<DatePicker.RangePicker />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
<Button style={{ marginLeft: 8 }} onClick={handleReset}>
</Button>
</Form.Item>
</Form>
<Table dataSource={workOrders} loading={loading} rowKey="id">
<Column title="工单编号" dataIndex="order_no" key="order_no" />
<Column title="设备名称" dataIndex="device_name" key="device_name" />
<Column title="问题描述" dataIndex="problem_desc" key="problem_desc" ellipsis />
<Column title="故障等级" dataIndex="priority" key="priority" />
<Column title="创建人" dataIndex="creator_name" key="creator_name" />
<Column
title="截止日期"
dataIndex="deadline"
key="deadline"
render={(deadline) => deadline ? dayjs(deadline).format('YYYY-MM-DD') : '-'}
/>
<Column
title="创建日期"
dataIndex="created_at"
key="created_at"
render={(date) => dayjs(date).format('YYYY-MM-DD')}
/>
<Column title="问题分类" dataIndex="problem_type" key="problem_type" />
<Column
title="状态"
dataIndex="status"
key="status"
render={(status, record: WorkOrder) => (
<Space>
<span>{status}</span>
{deadlineInfo && record.id === currentOrder?.id && (
<Tag color={deadlineInfo.color}>{deadlineInfo.text}</Tag>
)}
</Space>
)}
/>
<Column
title="结果反馈"
dataIndex="feedback"
key="feedback"
render={(feedback) => (
feedback ? (
<a onClick={() => {
setCurrentDetail(feedback);
setDetailModalVisible(true);
}}></a>
) : '-'
)}
/>
<Column
title="附件"
dataIndex="attachments"
key="attachments"
render={(attachments) => attachments?.length > 0 ? `${attachments.length}` : '无'}
/>
<Column
title="操作"
key="action"
render={(_, record: WorkOrder) => (
<Space size="middle">
<Dropdown
overlay={
<Menu>
{record.status === '待受理' && (
<Menu.Item key="accept" onClick={() => handleAccept(record.id)}>
</Menu.Item>
)}
{record.status === '处理中' && (
<Menu.Item key="reassign" onClick={() => handleReassign(record.id)}>
</Menu.Item>
)}
<Menu.Item
key="close"
onClick={() => handleClose(record.id)}
danger
>
</Menu.Item>
</Menu>
}
>
<Button type="primary" size="small"></Button>
</Dropdown>
<Button type="link" size="small" onClick={() => handleShowHistory(record.id)}>
</Button>
</Space>
)}
/>
</Table>
<Modal
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>{currentOrder?.id ? '工单详情' : '新建工单'}</div>
<Button
type="text"
icon={<CloseOutlined />}
onClick={() => setModalVisible(false)}
style={{ marginRight: -16 }}
/>
</div>
}
visible={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={800}
footer={
<div style={{ textAlign: 'center' }}>
<Button key="submit" type="primary" onClick={handleSubmit}>
</Button>
</div>
}
closable={false}
>
<Form form={form} layout="vertical">
<Form.Item name="order_no" label="工单编号" rules={[{ required: true }]}>
<Input placeholder="自动生成" disabled />
</Form.Item>
<Form.Item name="device_id" label="设备" rules={[{ required: true }]}>
<Select showSearch optionFilterProp="children">
{devices.map(device => (
<Option key={device.id} value={device.id}>
{device.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="problem_desc" label="问题描述" rules={[{ required: true }]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item name="priority" label="故障等级" rules={[{ required: true }]}>
<Select>
<Option value="紧急"></Option>
<Option value="高"></Option>
<Option value="中"></Option>
<Option value="低"></Option>
</Select>
</Form.Item>
<Form.Item name="deadline" label="截止日期" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="problem_type" label="问题分类">
<Select>
{categories.map(category => (
<Option key={category} value={category}>
{category}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="feedback" label="结果反馈">
<TextArea rows={2} />
</Form.Item>
<Form.Item label="附件">
<Uploader
onSuccess={handleUploadSuccess}
onError={(error) => message.error(`上传失败: ${error.message}`)}
onProgress={(percent) => (
<Progress percent={percent} status="active" />
)}
/>
{attachments.length > 0 && (
<List
dataSource={attachments}
renderItem={item => (
<List.Item>
<a href={item.url} target="_blank" rel="noopener noreferrer">
{item.name}
</a>
</List.Item>
)}
/>
)}
</Form.Item>
</Form>
{currentOrder?.id && (
<div style={{ marginTop: 24 }}>
<h3></h3>
<List
className="comment-list"
itemLayout="horizontal"
dataSource={comments}
renderItem={item => (
<List.Item>
<List.Item.Meta
avatar={<Avatar>{item.author.charAt(0)}</Avatar>}
title={item.author}
description={item.content}
/>
<div>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</div>
</List.Item>
)}
/>
<TextArea
rows={4}
value={commentContent}
onChange={(e) => setCommentContent(e.target.value)}
placeholder="输入评论内容"
/>
<Button
type="primary"
onClick={handleAddComment}
style={{ marginTop: 16 }}
>
</Button>
</div>
)}
</Modal>
<Modal
title={`工单流程记录 (共${statusHistory.length}条)`}
visible={historyVisible}
onCancel={() => setHistoryVisible(false)}
footer={null}
width={800}
>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
<Timeline mode="alternate">
{statusHistory.map(item => (
<Timeline.Item
key={item.id}
color={
item.status_to === '已完成' ? 'green' :
item.status_to === '已取消' ? 'red' : 'blue'
}
>
<div style={{ padding: '8px 16px', background: '#f9f9f9', borderRadius: 4 }}>
<strong>{item.status_from} {item.status_to}</strong>
<div style={{ marginTop: 8 }}>
<Tag color="geekblue">{item.operator}</Tag>
<Tag>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</Tag>
</div>
{item.comment && (
<div style={{ marginTop: 8 }}>
<p style={{ marginBottom: 0 }}><strong>:</strong></p>
<p style={{ marginTop: 4 }}>{item.comment}</p>
</div>
)}
</div>
</Timeline.Item>
))}
</Timeline>
</div>
</Modal>
<Modal
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Switch
style={{ marginLeft: 16 }}
checkedChildren="开"
unCheckedChildren="关"
defaultChecked
/>
</div>
<Button
type="text"
icon={<CloseOutlined />}
onClick={() => setAutoDispatchVisible(false)}
style={{ marginRight: -16 }}
/>
</div>
}
open={autoDispatchVisible}
footer={null}
width={600}
closable={false}
onCancel={() => setAutoDispatchVisible(false)}
>
<Form form={autoDispatchForm} layout="vertical">
<Form.Item name="device_type" label="设备分类">
<Select placeholder="全部类型设备" allowClear>
{categories.map(category => (
<Option key={category} value={category}>{category}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="problem_desc" label="问题描述">
<TextArea rows={3} placeholder="设备名称+告警故障" />
</Form.Item>
<Form.Item name="priority" label="故障等级" initialValue="中">
<Select>
<Option value="紧急"></Option>
<Option value="高"></Option>
<Option value="中"></Option>
<Option value="低"></Option>
</Select>
</Form.Item>
<Form.Item name="assignee" label="处理人" initialValue="admin">
<Select>
<Option value="admin">admin</Option>
<Option value="user1">1</Option>
<Option value="user2">2</Option>
<Option value="user3">3</Option>
</Select>
</Form.Item>
<Form.Item
name="deadline"
label="截止时间"
initialValue={dayjs().add(2, 'day')}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item>
<div style={{ textAlign: 'center', marginTop: 24 }}>
<Button
type="primary"
onClick={() => {
autoDispatchForm.validateFields()
.then(async values => {
try {
if (!values.problem_desc) {
values.problem_desc = `设备${values.device_type || '全部'}告警故障`;
}
await WorkOrderAPI.create({
...values,
title: '自动派工工单',
creator_id: 'system',
creator_name: '系统自动派工',
status: '待受理'
});
message.success('自动派工成功');
setAutoDispatchVisible(false);
fetchData();
} catch (error) {
message.error('自动派工失败');
}
})
.catch(info => {
console.log('Validate Failed:', info);
});
}}
style={{ width: 120 }}
>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
<Modal
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div></div>
<Button
type="text"
icon={<CloseOutlined />}
onClick={() => setDetailModalVisible(false)}
style={{ marginRight: -16 }}
/>
</div>
}
visible={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={null}
width={600}
closable={false}
>
<div style={{ padding: 16 }}>
{currentDetail}
</div>
</Modal>
</div>
);
}