pages_work_orders.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. import React, { useState, useEffect } from 'react';
  2. import { Button, Table, Space, Modal, Form, Input, Select, message, List, Avatar, Progress, Tag, Timeline, DatePicker, Switch, Dropdown, Menu } from 'antd';
  3. import type { MenuProps } from 'antd';
  4. import { CloseOutlined } from '@ant-design/icons';
  5. import dayjs from 'dayjs';
  6. import { WorkOrderAPI } from './api/work_orders.ts';
  7. import { DeviceInstanceAPI } from './api/device_instance.ts';
  8. import { Uploader } from './components_uploader.tsx';
  9. import { WorkOrderPriority, WorkOrderStatus } from '../../client/share/monitorTypes.ts';
  10. import type { WorkOrder, WorkOrderSettings, DeadlineInfo } from '../../client/share/monitorTypes.ts';
  11. const { Column } = Table;
  12. const { Option } = Select;
  13. const { TextArea } = Input;
  14. export function WorkOrdersPage() {
  15. const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
  16. const [settings, setSettings] = useState<WorkOrderSettings>();
  17. const [loading, setLoading] = useState(false);
  18. const [modalVisible, setModalVisible] = useState(false);
  19. const [currentOrder, setCurrentOrder] = useState<Partial<WorkOrder>>();
  20. const [categories, setCategories] = useState<string[]>([]);
  21. const [devices, setDevices] = useState<any[]>([]);
  22. const [attachments, setAttachments] = useState<any[]>([]);
  23. const [comments, setComments] = useState<any[]>([]);
  24. const [commentContent, setCommentContent] = useState('');
  25. const [form] = Form.useForm();
  26. const [historyVisible, setHistoryVisible] = useState(false);
  27. const [statusHistory, setStatusHistory] = useState<any[]>([]);
  28. const [deadlineInfo, setDeadlineInfo] = useState<DeadlineInfo>();
  29. const [autoDispatchVisible, setAutoDispatchVisible] = useState(false);
  30. const [autoDispatchForm] = Form.useForm();
  31. const [detailModalVisible, setDetailModalVisible] = useState(false);
  32. const [currentDetail, setCurrentDetail] = useState('');
  33. useEffect(() => {
  34. // 开发环境下生成模拟数据
  35. if (process.env.NODE_ENV === 'development') {
  36. const mockOrders = [
  37. {
  38. id: 'mock-1',
  39. title: '设备网络故障',
  40. order_no: `WO-${dayjs().format('YYYYMMDD')}-1001`,
  41. device_name: '网络交换机1',
  42. problem_desc: '设备无法连接网络',
  43. priority: WorkOrderPriority.URGENT,
  44. creator_id: 'system',
  45. creator_name: '系统管理员',
  46. deadline: dayjs().add(1, 'day').toISOString(),
  47. created_at: dayjs().toISOString(),
  48. updated_at: dayjs().toISOString(),
  49. problem_type: '网络',
  50. status: WorkOrderStatus.PENDING,
  51. feedback: '',
  52. attachments: []
  53. },
  54. {
  55. id: 'mock-2',
  56. title: '服务器硬件故障',
  57. order_no: `WO-${dayjs().format('YYYYMMDD')}-1002`,
  58. device_name: '服务器A',
  59. problem_desc: '硬盘故障需要更换',
  60. priority: WorkOrderPriority.IMPORTANT,
  61. creator_id: 'system',
  62. creator_name: '系统管理员',
  63. deadline: dayjs().add(2, 'day').toISOString(),
  64. created_at: dayjs().toISOString(),
  65. updated_at: dayjs().toISOString(),
  66. problem_type: '硬件',
  67. status: WorkOrderStatus.PROCESSING,
  68. feedback: '已订购新硬盘',
  69. attachments: []
  70. },
  71. {
  72. id: 'mock-3',
  73. title: '软件系统升级',
  74. order_no: `WO-${dayjs().format('YYYYMMDD')}-1003`,
  75. device_name: '办公电脑',
  76. problem_desc: '需要升级到最新版本',
  77. priority: WorkOrderPriority.NORMAL,
  78. creator_id: 'system',
  79. creator_name: '系统管理员',
  80. deadline: dayjs().add(3, 'day').toISOString(),
  81. created_at: dayjs().toISOString(),
  82. updated_at: dayjs().toISOString(),
  83. problem_type: '软件',
  84. status: WorkOrderStatus.CLOSED,
  85. feedback: '已完成升级',
  86. attachments: []
  87. },
  88. {
  89. id: 'mock-4',
  90. title: '打印机维护',
  91. order_no: `WO-${dayjs().format('YYYYMMDD')}-1004`,
  92. device_name: '办公室打印机',
  93. problem_desc: '定期维护保养',
  94. priority: WorkOrderPriority.NORMAL,
  95. creator_id: 'system',
  96. creator_name: '系统管理员',
  97. deadline: dayjs().add(4, 'day').toISOString(),
  98. created_at: dayjs().toISOString(),
  99. updated_at: dayjs().toISOString(),
  100. problem_type: '其他',
  101. status: WorkOrderStatus.PENDING,
  102. feedback: '',
  103. attachments: []
  104. },
  105. {
  106. id: 'mock-5',
  107. title: '数据库优化',
  108. order_no: `WO-${dayjs().format('YYYYMMDD')}-1005`,
  109. device_name: '数据库服务器',
  110. problem_desc: '查询性能优化',
  111. priority: WorkOrderPriority.IMPORTANT,
  112. creator_id: 'system',
  113. creator_name: '系统管理员',
  114. deadline: dayjs().add(5, 'day').toISOString(),
  115. created_at: dayjs().toISOString(),
  116. updated_at: dayjs().toISOString(),
  117. problem_type: '软件',
  118. status: WorkOrderStatus.PROCESSING,
  119. feedback: '正在优化索引',
  120. attachments: []
  121. }
  122. ];
  123. setWorkOrders(mockOrders);
  124. } else {
  125. fetchData();
  126. }
  127. fetchSettings();
  128. fetchCategories();
  129. fetchDevices();
  130. }, []);
  131. const [searchParams, setSearchParams] = useState({
  132. status: undefined,
  133. problemType: undefined,
  134. keyword: undefined,
  135. startDate: undefined,
  136. endDate: undefined
  137. });
  138. const fetchData = async () => {
  139. setLoading(true);
  140. try {
  141. const result = await WorkOrderAPI.getList(searchParams);
  142. setWorkOrders(result.data);
  143. } catch (error) {
  144. message.error('获取工单列表失败');
  145. } finally {
  146. setLoading(false);
  147. }
  148. };
  149. const handleSearch = (values: any) => {
  150. setSearchParams({
  151. status: values.status,
  152. problemType: values.problemType,
  153. keyword: values.keyword,
  154. startDate: values.dateRange?.[0]?.toISOString(),
  155. endDate: values.dateRange?.[1]?.toISOString()
  156. });
  157. };
  158. const handleReset = () => {
  159. setSearchParams({
  160. status: undefined,
  161. problemType: undefined,
  162. keyword: undefined,
  163. startDate: undefined,
  164. endDate: undefined
  165. });
  166. };
  167. const fetchSettings = async () => {
  168. try {
  169. const result = await WorkOrderAPI.getSettings();
  170. setSettings(result.data);
  171. } catch (error) {
  172. message.error('获取工单设置失败');
  173. }
  174. };
  175. const fetchCategories = async () => {
  176. try {
  177. const result = await WorkOrderAPI.getCategories();
  178. setCategories(result.data);
  179. } catch (error) {
  180. message.error('获取分类列表失败');
  181. }
  182. };
  183. const fetchDevices = async () => {
  184. try {
  185. const result = await DeviceInstanceAPI.getDeviceInstances();
  186. setDevices(result.data);
  187. } catch (error) {
  188. message.error('获取设备列表失败');
  189. }
  190. };
  191. const fetchComments = async (id: string) => {
  192. try {
  193. const result = await WorkOrderAPI.getComments(id);
  194. setComments(result.data);
  195. } catch (error) {
  196. message.error('获取评论失败');
  197. }
  198. };
  199. const fetchStatusHistory = async (id: string) => {
  200. try {
  201. const result = await WorkOrderAPI.getStatusHistory(id);
  202. setStatusHistory(result.data);
  203. } catch (error) {
  204. message.error('获取状态历史失败');
  205. }
  206. };
  207. const checkDeadline = async (order: WorkOrder) => {
  208. if (!order.deadline) return;
  209. try {
  210. const result = await WorkOrderAPI.getDeadline(order.id);
  211. const { remaining_hours, is_overdue } = result.data;
  212. let color = 'green';
  213. let text = '进行中';
  214. let progress = 100;
  215. if (is_overdue) {
  216. color = 'red';
  217. text = '已超时';
  218. progress = 0;
  219. } else if (remaining_hours < 24) {
  220. color = 'orange';
  221. text = `即将到期 (剩余${remaining_hours}小时)`;
  222. progress = Math.max(10, Math.min(90, remaining_hours * 4));
  223. }
  224. setDeadlineInfo({
  225. color,
  226. text,
  227. progress: Number(progress),
  228. remainingTime: `${remaining_hours}小时`,
  229. isOverdue: remaining_hours < 0,
  230. });
  231. } catch (error) {
  232. message.error('获取时限信息失败');
  233. }
  234. };
  235. const handleCreate = () => {
  236. setCurrentOrder({});
  237. setModalVisible(true);
  238. };
  239. const handleEdit = async (record: WorkOrder) => {
  240. setCurrentOrder(record);
  241. form.setFieldsValue(record);
  242. setModalVisible(true);
  243. checkDeadline(record);
  244. if (record.id) {
  245. await fetchComments(record.id);
  246. }
  247. };
  248. const handleSubmit = async () => {
  249. try {
  250. const values = await form.validateFields();
  251. if (currentOrder?.id) {
  252. await WorkOrderAPI.update(currentOrder.id, values);
  253. message.success('更新工单成功');
  254. } else {
  255. await WorkOrderAPI.create(values);
  256. message.success('创建工单成功');
  257. }
  258. setModalVisible(false);
  259. fetchData();
  260. } catch (error) {
  261. message.error('操作失败');
  262. }
  263. };
  264. const handleStatusChange = async (id: string, status: string) => {
  265. Modal.confirm({
  266. title: '确认状态变更',
  267. content: (
  268. <Form form={form}>
  269. <Form.Item name="comment" label="变更备注" rules={[{required: true}]}>
  270. <Input.TextArea placeholder="请输入状态变更原因" />
  271. </Form.Item>
  272. </Form>
  273. ),
  274. onOk: async (close) => {
  275. try {
  276. const values = await form.validateFields();
  277. await WorkOrderAPI.changeStatus(
  278. id,
  279. status,
  280. 'current_user', // TODO: 替换为实际用户
  281. values.comment
  282. );
  283. message.success('状态更新成功');
  284. fetchData();
  285. close();
  286. } catch (error) {
  287. message.error('状态更新失败');
  288. }
  289. }
  290. });
  291. };
  292. const renderStatusActions = (record: WorkOrder) => {
  293. const statusOptions = settings?.statusOptions || [];
  294. const currentStatusIndex = statusOptions.indexOf(record.status);
  295. const nextStatus = statusOptions[currentStatusIndex + 1];
  296. const prevStatus = statusOptions[currentStatusIndex - 1];
  297. return (
  298. <Space>
  299. {prevStatus && (
  300. <Button
  301. size="small"
  302. onClick={() => handleStatusChange(record.id, prevStatus)}
  303. >
  304. 回退到{prevStatus}
  305. </Button>
  306. )}
  307. {nextStatus && (
  308. <Button
  309. type="primary"
  310. size="small"
  311. onClick={() => handleStatusChange(record.id, nextStatus)}
  312. >
  313. 推进到{nextStatus}
  314. </Button>
  315. )}
  316. {!nextStatus && !prevStatus && (
  317. <span>无可用操作</span>
  318. )}
  319. </Space>
  320. );
  321. };
  322. const handleShowHistory = async (id: string) => {
  323. await fetchStatusHistory(id);
  324. setHistoryVisible(true);
  325. };
  326. const handleAssign = async (id: string, assignee: string) => {
  327. try {
  328. await WorkOrderAPI.assign(id, assignee);
  329. message.success('分配成功');
  330. fetchData();
  331. } catch (error) {
  332. message.error('分配失败');
  333. }
  334. };
  335. const handleUploadSuccess = (fileUrl: string, fileInfo: any) => {
  336. if (currentOrder?.id) {
  337. setAttachments(prev => [...prev, {
  338. id: fileInfo.id,
  339. url: fileUrl,
  340. name: fileInfo.original_filename
  341. }]);
  342. message.success('附件上传成功');
  343. }
  344. };
  345. const handleAddComment = async () => {
  346. if (!currentOrder?.id) {
  347. message.error('请先保存工单');
  348. return;
  349. }
  350. if (!commentContent.trim()) {
  351. message.error('评论内容不能为空');
  352. return;
  353. }
  354. if (commentContent.length > 500) {
  355. message.error('评论内容不能超过500字');
  356. return;
  357. }
  358. // 简单敏感词过滤
  359. const bannedWords = ['敏感词1', '敏感词2', '敏感词3'];
  360. if (bannedWords.some(word => commentContent.includes(word))) {
  361. message.error('评论包含不允许的内容');
  362. return;
  363. }
  364. try {
  365. await WorkOrderAPI.addComment(currentOrder.id, commentContent);
  366. setCommentContent('');
  367. await fetchComments(currentOrder.id);
  368. message.success('评论添加成功');
  369. } catch (error) {
  370. message.error('评论添加失败');
  371. }
  372. };
  373. const handleAccept = async (id: string) => {
  374. try {
  375. await WorkOrderAPI.changeStatus(id, '处理中', 'current_user', '工单已受理');
  376. message.success('工单受理成功');
  377. fetchData();
  378. } catch (error) {
  379. message.error('受理失败');
  380. }
  381. };
  382. const handleReassign = async (id: string) => {
  383. Modal.confirm({
  384. title: '改派工单',
  385. content: (
  386. <Select placeholder="选择新的处理人" style={{ width: '100%' }}>
  387. <Option value="user1">用户1</Option>
  388. <Option value="user2">用户2</Option>
  389. <Option value="user3">用户3</Option>
  390. </Select>
  391. ),
  392. onOk: async (close) => {
  393. try {
  394. await WorkOrderAPI.assign(id, 'new_assignee'); // TODO: 替换为实际选择的值
  395. message.success('工单改派成功');
  396. fetchData();
  397. close();
  398. } catch (error) {
  399. message.error('改派失败');
  400. }
  401. }
  402. });
  403. };
  404. const handleClose = async (id: string) => {
  405. Modal.confirm({
  406. title: '关闭工单',
  407. content: (
  408. <Form form={form}>
  409. <Form.Item name="feedback" label="处理结果" rules={[{required: true}]}>
  410. <TextArea placeholder="请输入处理结果反馈" />
  411. </Form.Item>
  412. </Form>
  413. ),
  414. onOk: async (close) => {
  415. try {
  416. const values = await form.validateFields();
  417. await WorkOrderAPI.changeStatus(
  418. id,
  419. '已关闭',
  420. 'current_user',
  421. values.feedback
  422. );
  423. message.success('工单已关闭');
  424. fetchData();
  425. close();
  426. } catch (error) {
  427. message.error('关闭失败');
  428. }
  429. }
  430. });
  431. };
  432. return (
  433. <div>
  434. <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
  435. <Space>
  436. <Button type="primary" onClick={handleCreate}>
  437. 新建工单
  438. </Button>
  439. <Button type="primary" onClick={() => setAutoDispatchVisible(true)}>
  440. 自动派工
  441. </Button>
  442. </Space>
  443. <Space>
  444. <Button
  445. type="primary"
  446. onClick={async () => {
  447. try {
  448. const data = await WorkOrderAPI.exportList(searchParams);
  449. const url = window.URL.createObjectURL(new Blob([data]));
  450. const link = document.createElement('a');
  451. link.href = url;
  452. link.setAttribute('download', `工单列表_${dayjs().format('YYYYMMDD')}.xlsx`);
  453. document.body.appendChild(link);
  454. link.click();
  455. document.body.removeChild(link);
  456. message.success('导出成功');
  457. } catch (error) {
  458. message.error('导出失败');
  459. }
  460. }}
  461. >
  462. 工单导出
  463. </Button>
  464. </Space>
  465. </div>
  466. <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
  467. <Form.Item name="status" label="工单状态">
  468. <Select style={{ width: 120 }} allowClear>
  469. {Object.values(WorkOrderStatus).map(status => (
  470. <Option key={status} value={status}>{status}</Option>
  471. ))}
  472. </Select>
  473. </Form.Item>
  474. <Form.Item name="problemType" label="问题分类">
  475. <Select style={{ width: 120 }} allowClear>
  476. {categories.map(category => (
  477. <Option key={category} value={category}>{category}</Option>
  478. ))}
  479. </Select>
  480. </Form.Item>
  481. <Form.Item name="keyword" label="关键字">
  482. <Input placeholder="请输入关键字" />
  483. </Form.Item>
  484. <Form.Item name="dateRange" label="时间范围">
  485. <DatePicker.RangePicker />
  486. </Form.Item>
  487. <Form.Item>
  488. <Button type="primary" htmlType="submit">
  489. 查询
  490. </Button>
  491. <Button style={{ marginLeft: 8 }} onClick={handleReset}>
  492. 重置
  493. </Button>
  494. </Form.Item>
  495. </Form>
  496. <Table dataSource={workOrders} loading={loading} rowKey="id">
  497. <Column title="工单编号" dataIndex="order_no" key="order_no" />
  498. <Column title="设备名称" dataIndex="device_name" key="device_name" />
  499. <Column title="问题描述" dataIndex="problem_desc" key="problem_desc" ellipsis />
  500. <Column title="故障等级" dataIndex="priority" key="priority" />
  501. <Column title="创建人" dataIndex="creator_name" key="creator_name" />
  502. <Column
  503. title="截止日期"
  504. dataIndex="deadline"
  505. key="deadline"
  506. render={(deadline) => deadline ? dayjs(deadline).format('YYYY-MM-DD') : '-'}
  507. />
  508. <Column
  509. title="创建日期"
  510. dataIndex="created_at"
  511. key="created_at"
  512. render={(date) => dayjs(date).format('YYYY-MM-DD')}
  513. />
  514. <Column title="问题分类" dataIndex="problem_type" key="problem_type" />
  515. <Column
  516. title="状态"
  517. dataIndex="status"
  518. key="status"
  519. render={(status, record: WorkOrder) => (
  520. <Space>
  521. <span>{status}</span>
  522. {deadlineInfo && record.id === currentOrder?.id && (
  523. <Tag color={deadlineInfo.color}>{deadlineInfo.text}</Tag>
  524. )}
  525. </Space>
  526. )}
  527. />
  528. <Column
  529. title="结果反馈"
  530. dataIndex="feedback"
  531. key="feedback"
  532. render={(feedback) => (
  533. feedback ? (
  534. <a onClick={() => {
  535. setCurrentDetail(feedback);
  536. setDetailModalVisible(true);
  537. }}>详情</a>
  538. ) : '-'
  539. )}
  540. />
  541. <Column
  542. title="附件"
  543. dataIndex="attachments"
  544. key="attachments"
  545. render={(attachments) => attachments?.length > 0 ? `${attachments.length}个` : '无'}
  546. />
  547. <Column
  548. title="操作"
  549. key="action"
  550. render={(_, record: WorkOrder) => (
  551. <Space size="middle">
  552. <Dropdown
  553. overlay={
  554. <Menu>
  555. {record.status === '待受理' && (
  556. <Menu.Item key="accept" onClick={() => handleAccept(record.id)}>
  557. 受理
  558. </Menu.Item>
  559. )}
  560. {record.status === '处理中' && (
  561. <Menu.Item key="reassign" onClick={() => handleReassign(record.id)}>
  562. 改派
  563. </Menu.Item>
  564. )}
  565. <Menu.Item
  566. key="close"
  567. onClick={() => handleClose(record.id)}
  568. danger
  569. >
  570. 关闭
  571. </Menu.Item>
  572. </Menu>
  573. }
  574. >
  575. <Button type="primary" size="small">操作</Button>
  576. </Dropdown>
  577. <Button type="link" size="small" onClick={() => handleShowHistory(record.id)}>
  578. 流程
  579. </Button>
  580. </Space>
  581. )}
  582. />
  583. </Table>
  584. <Modal
  585. title={
  586. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
  587. <div>{currentOrder?.id ? '工单详情' : '新建工单'}</div>
  588. <Button
  589. type="text"
  590. icon={<CloseOutlined />}
  591. onClick={() => setModalVisible(false)}
  592. style={{ marginRight: -16 }}
  593. />
  594. </div>
  595. }
  596. visible={modalVisible}
  597. onOk={handleSubmit}
  598. onCancel={() => setModalVisible(false)}
  599. width={800}
  600. footer={
  601. <div style={{ textAlign: 'center' }}>
  602. <Button key="submit" type="primary" onClick={handleSubmit}>
  603. 确定
  604. </Button>
  605. </div>
  606. }
  607. closable={false}
  608. >
  609. <Form form={form} layout="vertical">
  610. <Form.Item name="order_no" label="工单编号" rules={[{ required: true }]}>
  611. <Input placeholder="自动生成" disabled />
  612. </Form.Item>
  613. <Form.Item name="device_id" label="设备" rules={[{ required: true }]}>
  614. <Select showSearch optionFilterProp="children">
  615. {devices.map(device => (
  616. <Option key={device.id} value={device.id}>
  617. {device.name}
  618. </Option>
  619. ))}
  620. </Select>
  621. </Form.Item>
  622. <Form.Item name="problem_desc" label="问题描述" rules={[{ required: true }]}>
  623. <TextArea rows={4} />
  624. </Form.Item>
  625. <Form.Item name="priority" label="故障等级" rules={[{ required: true }]}>
  626. <Select>
  627. <Option value="紧急">紧急</Option>
  628. <Option value="高">高</Option>
  629. <Option value="中">中</Option>
  630. <Option value="低">低</Option>
  631. </Select>
  632. </Form.Item>
  633. <Form.Item name="deadline" label="截止日期" rules={[{ required: true }]}>
  634. <DatePicker style={{ width: '100%' }} />
  635. </Form.Item>
  636. <Form.Item name="problem_type" label="问题分类">
  637. <Select>
  638. {categories.map(category => (
  639. <Option key={category} value={category}>
  640. {category}
  641. </Option>
  642. ))}
  643. </Select>
  644. </Form.Item>
  645. <Form.Item name="feedback" label="结果反馈">
  646. <TextArea rows={2} />
  647. </Form.Item>
  648. <Form.Item label="附件">
  649. <Uploader
  650. onSuccess={handleUploadSuccess}
  651. onError={(error) => message.error(`上传失败: ${error.message}`)}
  652. onProgress={(percent) => (
  653. <Progress percent={percent} status="active" />
  654. )}
  655. />
  656. {attachments.length > 0 && (
  657. <List
  658. dataSource={attachments}
  659. renderItem={item => (
  660. <List.Item>
  661. <a href={item.url} target="_blank" rel="noopener noreferrer">
  662. {item.name}
  663. </a>
  664. </List.Item>
  665. )}
  666. />
  667. )}
  668. </Form.Item>
  669. </Form>
  670. {currentOrder?.id && (
  671. <div style={{ marginTop: 24 }}>
  672. <h3>评论</h3>
  673. <List
  674. className="comment-list"
  675. itemLayout="horizontal"
  676. dataSource={comments}
  677. renderItem={item => (
  678. <List.Item>
  679. <List.Item.Meta
  680. avatar={<Avatar>{item.author.charAt(0)}</Avatar>}
  681. title={item.author}
  682. description={item.content}
  683. />
  684. <div>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</div>
  685. </List.Item>
  686. )}
  687. />
  688. <TextArea
  689. rows={4}
  690. value={commentContent}
  691. onChange={(e) => setCommentContent(e.target.value)}
  692. placeholder="输入评论内容"
  693. />
  694. <Button
  695. type="primary"
  696. onClick={handleAddComment}
  697. style={{ marginTop: 16 }}
  698. >
  699. 提交评论
  700. </Button>
  701. </div>
  702. )}
  703. </Modal>
  704. <Modal
  705. title={`工单流程记录 (共${statusHistory.length}条)`}
  706. visible={historyVisible}
  707. onCancel={() => setHistoryVisible(false)}
  708. footer={null}
  709. width={800}
  710. >
  711. <div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
  712. <Timeline mode="alternate">
  713. {statusHistory.map(item => (
  714. <Timeline.Item
  715. key={item.id}
  716. color={
  717. item.status_to === '已完成' ? 'green' :
  718. item.status_to === '已取消' ? 'red' : 'blue'
  719. }
  720. >
  721. <div style={{ padding: '8px 16px', background: '#f9f9f9', borderRadius: 4 }}>
  722. <strong>{item.status_from} → {item.status_to}</strong>
  723. <div style={{ marginTop: 8 }}>
  724. <Tag color="geekblue">{item.operator}</Tag>
  725. <Tag>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</Tag>
  726. </div>
  727. {item.comment && (
  728. <div style={{ marginTop: 8 }}>
  729. <p style={{ marginBottom: 0 }}><strong>备注:</strong></p>
  730. <p style={{ marginTop: 4 }}>{item.comment}</p>
  731. </div>
  732. )}
  733. </div>
  734. </Timeline.Item>
  735. ))}
  736. </Timeline>
  737. </div>
  738. </Modal>
  739. <Modal
  740. title={
  741. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
  742. <div>
  743. 自动派工
  744. <Switch
  745. style={{ marginLeft: 16 }}
  746. checkedChildren="开"
  747. unCheckedChildren="关"
  748. defaultChecked
  749. />
  750. </div>
  751. <Button
  752. type="text"
  753. icon={<CloseOutlined />}
  754. onClick={() => setAutoDispatchVisible(false)}
  755. style={{ marginRight: -16 }}
  756. />
  757. </div>
  758. }
  759. open={autoDispatchVisible}
  760. footer={null}
  761. width={600}
  762. closable={false}
  763. onCancel={() => setAutoDispatchVisible(false)}
  764. >
  765. <Form form={autoDispatchForm} layout="vertical">
  766. <Form.Item name="device_type" label="设备分类">
  767. <Select placeholder="全部类型设备" allowClear>
  768. {categories.map(category => (
  769. <Option key={category} value={category}>{category}</Option>
  770. ))}
  771. </Select>
  772. </Form.Item>
  773. <Form.Item name="problem_desc" label="问题描述">
  774. <TextArea rows={3} placeholder="设备名称+告警故障" />
  775. </Form.Item>
  776. <Form.Item name="priority" label="故障等级" initialValue="中">
  777. <Select>
  778. <Option value="紧急">紧急</Option>
  779. <Option value="高">高</Option>
  780. <Option value="中">中</Option>
  781. <Option value="低">低</Option>
  782. </Select>
  783. </Form.Item>
  784. <Form.Item name="assignee" label="处理人" initialValue="admin">
  785. <Select>
  786. <Option value="admin">admin</Option>
  787. <Option value="user1">用户1</Option>
  788. <Option value="user2">用户2</Option>
  789. <Option value="user3">用户3</Option>
  790. </Select>
  791. </Form.Item>
  792. <Form.Item
  793. name="deadline"
  794. label="截止时间"
  795. initialValue={dayjs().add(2, 'day')}
  796. >
  797. <DatePicker style={{ width: '100%' }} />
  798. </Form.Item>
  799. <Form.Item>
  800. <div style={{ textAlign: 'center', marginTop: 24 }}>
  801. <Button
  802. type="primary"
  803. onClick={() => {
  804. autoDispatchForm.validateFields()
  805. .then(async values => {
  806. try {
  807. if (!values.problem_desc) {
  808. values.problem_desc = `设备${values.device_type || '全部'}告警故障`;
  809. }
  810. await WorkOrderAPI.create({
  811. ...values,
  812. title: '自动派工工单',
  813. creator_id: 'system',
  814. creator_name: '系统自动派工',
  815. status: '待受理'
  816. });
  817. message.success('自动派工成功');
  818. setAutoDispatchVisible(false);
  819. fetchData();
  820. } catch (error) {
  821. message.error('自动派工失败');
  822. }
  823. })
  824. .catch(info => {
  825. console.log('Validate Failed:', info);
  826. });
  827. }}
  828. style={{ width: 120 }}
  829. >
  830. 确认
  831. </Button>
  832. </div>
  833. </Form.Item>
  834. </Form>
  835. </Modal>
  836. <Modal
  837. title={
  838. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
  839. <div>工单详情</div>
  840. <Button
  841. type="text"
  842. icon={<CloseOutlined />}
  843. onClick={() => setDetailModalVisible(false)}
  844. style={{ marginRight: -16 }}
  845. />
  846. </div>
  847. }
  848. visible={detailModalVisible}
  849. onCancel={() => setDetailModalVisible(false)}
  850. footer={null}
  851. width={600}
  852. closable={false}
  853. >
  854. <div style={{ padding: 16 }}>
  855. {currentDetail}
  856. </div>
  857. </Modal>
  858. </div>
  859. );
  860. }