904 lines
29 KiB
TypeScript
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>
|
|
);
|
|
} |