feat(工单管理): 添加工单导出功能
实现工单数据的导出功能,支持CSV、JSON和Excel格式。前端添加导出模态框和选择逻辑,后端实现数据处理和文件生成。用户可选择导出选中项、当前页或全部工单。 - 前端添加TicketExportModal组件处理导出选项 - 后端添加/export接口处理不同格式的导出请求 - 支持导出工单基础信息和自定义字段 - 添加导出按钮到工单管理页面
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Form, Select, Button, Space, message } from 'antd';
|
||||
import { ExportOutlined, FileTextOutlined, BranchesOutlined, TableOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const modalHeaderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const TicketExportModal = ({
|
||||
visible,
|
||||
onExport,
|
||||
onCancel,
|
||||
selectedCount = 0,
|
||||
currentPageCount = 0,
|
||||
totalCount = 0,
|
||||
}) => {
|
||||
const [exportFormat, setExportFormat] = useState('csv');
|
||||
const [exportScope, setExportScope] = useState('selected');
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
if (exportScope === 'selected' && selectedCount === 0) {
|
||||
message.warning('请先选择要导出的工单');
|
||||
return;
|
||||
}
|
||||
setExportLoading(true);
|
||||
try {
|
||||
await onExport({
|
||||
format: exportFormat,
|
||||
scope: exportScope,
|
||||
});
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
message.error('导出失败');
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
<ExportOutlined style={{ color: '#fa8c16' }} />
|
||||
导出工单数据
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
height: '40px',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={exportLoading}
|
||||
onClick={handleExport}
|
||||
style={{
|
||||
height: '40px',
|
||||
borderRadius: '6px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontWeight: '500',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
导出
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
body: { padding: '24px' },
|
||||
}}
|
||||
width={480}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item
|
||||
label={<span style={{ fontWeight: 500 }}>导出格式</span>}
|
||||
>
|
||||
<Select
|
||||
value={exportFormat}
|
||||
onChange={setExportFormat}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Option value="csv">
|
||||
<Space>
|
||||
<FileTextOutlined />
|
||||
CSV 格式(适合Excel打开)
|
||||
</Space>
|
||||
</Option>
|
||||
<Option value="json">
|
||||
<Space>
|
||||
<BranchesOutlined />
|
||||
JSON 格式(适合程序处理)
|
||||
</Space>
|
||||
</Option>
|
||||
<Option value="xlsx">
|
||||
<Space>
|
||||
<TableOutlined />
|
||||
Excel 格式(.xlsx)
|
||||
</Space>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ fontWeight: 500 }}>导出范围</span>}
|
||||
>
|
||||
<Select
|
||||
value={exportScope}
|
||||
onChange={setExportScope}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Option value="selected">选中工单 ({selectedCount} 个)</Option>
|
||||
<Option value="currentPage">当前页 ({currentPageCount} 个)</Option>
|
||||
<Option value="all">全部工单 ({totalCount} 个)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: '#f6f7ff',
|
||||
border: '1px solid #e8eaff',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
color: '#666',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
将导出工单的所有字段,包括基础信息和自定义字段
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TicketExportModal);
|
||||
@@ -41,9 +41,11 @@ import {
|
||||
DatabaseOutlined,
|
||||
EnvironmentOutlined,
|
||||
TagOutlined,
|
||||
ExportOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import TicketExportModal from '../components/TicketExportModal';
|
||||
import dayjs from 'dayjs';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { debounce, getUserFromStorage } from '../utils/common';
|
||||
@@ -246,6 +248,8 @@ function TicketManagement() {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [processingModalVisible, setProcessingModalVisible] = useState(false);
|
||||
const [exportModalVisible, setExportModalVisible] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [editingTicket, setEditingTicket] = useState(null);
|
||||
const [selectedTicket, setSelectedTicket] = useState(null);
|
||||
const [operationRecords, setOperationRecords] = useState([]);
|
||||
@@ -815,6 +819,69 @@ function TicketManagement() {
|
||||
fetchTickets(1, pagination.pageSize, {});
|
||||
}, [fetchTickets, pagination.pageSize]);
|
||||
|
||||
const handleExport = useCallback(
|
||||
async ({ format, scope }) => {
|
||||
try {
|
||||
let ticketIds = [];
|
||||
|
||||
if (scope === 'selected') {
|
||||
ticketIds = selectedRowKeys;
|
||||
} else if (scope === 'currentPage') {
|
||||
ticketIds = tickets.map(t => t.ticketId);
|
||||
} else {
|
||||
ticketIds = tickets.map(t => t.ticketId);
|
||||
}
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
message.warning('没有可导出的工单');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
ticketIds.forEach(id => params.append('ticketIds', id));
|
||||
params.append('format', format);
|
||||
Object.entries(searchFilters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axios.get(`/api/tickets/export?${params.toString()}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
let mimeType;
|
||||
let filename;
|
||||
if (format === 'json') {
|
||||
mimeType = 'application/json';
|
||||
filename = `tickets_${Date.now()}.json`;
|
||||
} else if (format === 'xlsx') {
|
||||
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
filename = `tickets_${Date.now()}.xlsx`;
|
||||
} else {
|
||||
mimeType = 'text/csv;charset=utf-8';
|
||||
filename = `tickets_${Date.now()}.csv`;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: mimeType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.success(`成功导出 ${ticketIds.length} 个工单`);
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
message.error('导出失败');
|
||||
}
|
||||
},
|
||||
[searchFilters, selectedRowKeys, tickets]
|
||||
);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
paginationInfo => {
|
||||
setPagination(paginationInfo);
|
||||
@@ -1070,9 +1137,14 @@ function TicketManagement() {
|
||||
<Card
|
||||
title="工单管理"
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||
创建工单
|
||||
</Button>
|
||||
<Space>
|
||||
<Button icon={<ExportOutlined />} onClick={() => setExportModalVisible(true)}>
|
||||
导出
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||
创建工单
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
@@ -1127,6 +1199,10 @@ function TicketManagement() {
|
||||
loading={loading}
|
||||
onChange={handleTableChange}
|
||||
scroll={{ x: 1200 }}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
}}
|
||||
columnsState={{
|
||||
onChange: ({ visibleColumns }) => {
|
||||
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns);
|
||||
@@ -1952,6 +2028,15 @@ function TicketManagement() {
|
||||
</Tabs>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<TicketExportModal
|
||||
visible={exportModalVisible}
|
||||
onExport={handleExport}
|
||||
onCancel={() => setExportModalVisible(false)}
|
||||
selectedCount={selectedRowKeys.length}
|
||||
currentPageCount={tickets.length}
|
||||
totalCount={pagination.total}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user