/* * 开源代码,仅供学习和交流研究使用,商用请联系三丙 * 微信:mohan_88888 * 抖音:程序员三丙 * 付费课程知识星球:https://t.zsxq.com/aKtXo */ import React, {useEffect, useMemo, useState} from 'react'; import { Button, Card, Cascader, Checkbox, Col, Dropdown, Form, Input, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd'; import {DeleteOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, TableOutlined} from '@ant-design/icons'; import type {ColumnsType, TableProps} from 'antd/es/table'; import {formatTimestamp, generateGunCode, showMessage} from '../utils'; import {getErrorMessage} from '../services/api'; import * as gunService from '../services/gunService'; import * as stationService from '../services/stationService'; import {pileService} from '../services/pileService'; import type {Gun, GunCreateRequest, GunUpdateRequest, PileOption, StationOption} from '../types'; const { confirm } = Modal; const GunManagement: React.FC = () => { const [dataSource, setDataSource] = useState([]); const [loading, setLoading] = useState(false); const [searchForm] = Form.useForm(); const [form] = Form.useForm(); const [stationOptions, setStationOptions] = useState([]); const [pileOptions, setPileOptions] = useState([]); const [cascaderOptions, setCascaderOptions] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [modalLoading, setModalLoading] = useState(false); const [isEdit, setIsEdit] = useState(false); const [currentRecord, setCurrentRecord] = useState(null); // 分页和搜索状态 const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showQuickJumper: true, showTotal: (total: number) => `共 ${total} 条记录` }); const [searchParams, setSearchParams] = useState<{ page: number; size: number; gunName?: string; gunCode?: string; gunNo?: string; stationId?: string; sortField?: string; sortOrder?: string; }>({ page: 1, size: 10 }); // 批量删除相关状态 const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchDeleting, setBatchDeleting] = useState(false); // 列可见性配置 interface ColumnConfig { key: string; title: string; defaultVisible: boolean; } const columnConfigs: ColumnConfig[] = [ { key: 'gunName', title: '充电枪名称', defaultVisible: true }, { key: 'gunCode', title: '充电枪编码', defaultVisible: true }, { key: 'gunNo', title: '枪号', defaultVisible: true }, { key: 'stationName', title: '所属充电站', defaultVisible: true }, { key: 'pileName', title: '所属充电桩', defaultVisible: true }, { key: 'runStatus', title: '运行状态', defaultVisible: true }, { key: 'createdTime', title: '创建时间', defaultVisible: true }, { key: 'updatedTime', title: '更新时间', defaultVisible: false }, ]; // 列可见性状态 const [visibleColumns, setVisibleColumns] = useState>(() => { const defaultVisible: Record = {}; columnConfigs.forEach(config => { defaultVisible[config.key] = config.defaultVisible; }); return defaultVisible; }); // 列顺序状态(不包含action列,action列始终在最后) const [columnOrder, setColumnOrder] = useState(() => { return columnConfigs.map(config => config.key); }); // 完整的表格列定义 const allColumns: ColumnsType = useMemo(() => [ { title: '充电枪名称', dataIndex: 'gunName', key: 'gunName', width: 200, sorter: true, }, { title: '充电枪编码', dataIndex: 'gunCode', key: 'gunCode', width: 150, sorter: true, }, { title: '枪号', dataIndex: 'gunNo', key: 'gunNo', width: 55, sorter: true, }, { title: '所属充电站', dataIndex: 'stationName', key: 'stationName', width: 150, sorter: true, render: (stationName: string) => stationName || '-', }, { title: '所属充电桩', dataIndex: 'pileName', key: 'pileName', width: 150, sorter: true, render: (pileName: string, record: Gun) => (
{pileName || record.pileCode || '-'}
{record.pileCode && pileName && (
{record.pileCode}
)}
), }, { title: '运行状态', dataIndex: 'runStatus', key: 'runStatus', width: 100, render: (status: string) => { const getRunStatusColor = (status: string) => { const colors: Record = { 'IDLE': 'green', 'INSERTED': 'orange', 'CHARGING': 'blue', 'CHARGE_COMPLETE': 'cyan', 'DISCHARGE_READY': 'purple', 'DISCHARGING': 'magenta', 'DISCHARGE_COMPLETE': 'lime', 'RESERVED': 'geekblue', 'FAULT': 'red' }; return colors[status] || 'default'; }; const getRunStatusText = (status: string) => { const texts: Record = { 'IDLE': '空闲', 'INSERTED': '已插枪', 'CHARGING': '充电中', 'CHARGE_COMPLETE': '充电完成', 'DISCHARGE_READY': '放电准备', 'DISCHARGING': '放电中', 'DISCHARGE_COMPLETE': '放电完成', 'RESERVED': '预约中', 'FAULT': '故障' }; return texts[status] || status; }; return {getRunStatusText(status)}; }, }, { title: '创建时间', dataIndex: 'createdTime', key: 'createdTime', width: 95, sorter: true, render: (timestamp: number) => { const formatted = formatTimestamp(timestamp); if (!formatted || formatted === '-') return formatted; const parts = formatted.split(' '); return (
{parts[0]}
{parts[1]}
); } }, { title: '更新时间', dataIndex: 'updatedTime', key: 'updatedTime', width: 95, sorter: true, render: (timestamp: number) => { const formatted = formatTimestamp(timestamp); if (!formatted || formatted === '-') return formatted; const parts = formatted.split(' '); return (
{parts[0]}
{parts[1]}
); } }, { title: '操作', key: 'action', width: 150, fixed: 'right', render: (record: Gun) => (

确定要删除充电枪 {record.gunName} 吗?

此操作不可撤销,请谨慎操作!

} onConfirm={() => handleDelete(record)} okText="确定删除" okType="danger" cancelText="取消" >
), }, // eslint-disable-next-line react-hooks/exhaustive-deps ], []); // 根据可见性和顺序过滤并排序列 const visibleColumnsData = useMemo(() => { // 先按照用户定义的顺序排序(不包含action) const orderedColumns = columnOrder.map(key => { return allColumns.find(col => col.key === key); }).filter(Boolean) as ColumnsType; // 过滤出可见的列 const filtered = orderedColumns.filter(column => { return visibleColumns[column.key as string]; }); // 找到操作列并确保始终在最后 const actionColumn = allColumns.find(col => col.key === 'action'); return actionColumn ? [...filtered, actionColumn] : filtered; }, [visibleColumns, columnOrder, allColumns]); // 列选择器变更处理 const handleColumnVisibilityChange = (checkedValues: string[]) => { const newVisibleColumns: Record = {}; columnConfigs.forEach(config => { newVisibleColumns[config.key] = checkedValues.includes(config.key); }); setVisibleColumns(newVisibleColumns); }; // 移动列顺序 const moveColumn = (index: number, direction: 'up' | 'down') => { const visibleKeys = columnOrder.filter(key => visibleColumns[key]); const currentKey = visibleKeys[index]; const targetIndex = direction === 'up' ? index - 1 : index + 1; if (targetIndex >= 0 && targetIndex < visibleKeys.length) { const targetKey = visibleKeys[targetIndex]; // 在原始顺序中交换位置 const newOrder = [...columnOrder]; const currentOriginalIndex = newOrder.indexOf(currentKey); const targetOriginalIndex = newOrder.indexOf(targetKey); [newOrder[currentOriginalIndex], newOrder[targetOriginalIndex]] = [newOrder[targetOriginalIndex], newOrder[currentOriginalIndex]]; setColumnOrder(newOrder); } }; // 列选择器菜单 const columnSelectorMenu = { items: [ { key: 'column-selector', label: (
e.stopPropagation()}> 自定义列显示 {/* 列可见性选择 */}
选择显示列: visibleColumns[key])} onChange={handleColumnVisibilityChange} style={{ width: '100%' }} >
{columnOrder.map(key => { const config = columnConfigs.find(c => c.key === key); if (!config) return null; return ( {config.title} ); })}
{/* 列顺序调整 */}
调整列顺序:
{columnOrder.filter(key => visibleColumns[key]).map((key, index, visibleKeys) => { const config = columnConfigs.find(c => c.key === key); if (!config) return null; return (
{config.title}
); })}
), }, ], }; // 加载数据 const loadData = async () => { setLoading(true); try { const response = await gunService.getGuns(searchParams); const { records, total } = response; setDataSource(records); setPagination(prev => ({ ...prev, current: searchParams.page, pageSize: searchParams.size, total })); } catch (error: any) { console.error('加载充电枪数据失败:', error); const errorMessage = getErrorMessage(error); showMessage.error(errorMessage); } finally { setLoading(false); } }; // 加载充电站选项 const loadStationOptions = async () => { try { const response = await stationService.getStationOptions(); setStationOptions(Array.isArray(response) ? response : []); } catch (error: any) { console.error('加载充电站选项失败:', error); } }; // 加载充电桩选项 const loadPileOptions = async () => { try { const response = await pileService.getPileOptions(); setPileOptions(response.data || []); } catch (error: any) { console.error('加载充电桩选项失败:', error); } }; // 加载级联选择器数据 const loadCascaderOptions = async () => { try { const response = await stationService.getStationPileCascaderOptions(); setCascaderOptions(Array.isArray(response) ? response : []); } catch (error: any) { console.error('加载级联选择器数据失败:', error); } }; // 监听搜索参数变化 useEffect(() => { loadData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); // 初始化加载充电站选项和充电桩选项 useEffect(() => { loadStationOptions(); loadPileOptions(); loadCascaderOptions(); }, []); // 处理表格变化 const handleTableChange: TableProps['onChange'] = (pag, filters, sorter) => { let newParams = { ...searchParams, page: pag.current || 1, size: pag.pageSize || 10 }; // 处理排序 if (sorter && !Array.isArray(sorter) && sorter.field) { newParams.sortField = sorter.field as string; newParams.sortOrder = sorter.order === 'ascend' ? 'asc' : 'desc'; } else { delete newParams.sortField; delete newParams.sortOrder; } setSearchParams(newParams); }; // 搜索处理 const handleSearch = (values: any) => { const newParams = { page: 1, size: pagination.pageSize, ...values }; setSearchParams(newParams); }; // 重置搜索 const handleReset = () => { searchForm.resetFields(); const newParams = { page: 1, size: pagination.pageSize }; setSearchParams(newParams); }; // 显示新建模态框 const showCreateModal = () => { setIsEdit(false); setCurrentRecord(null); setModalVisible(true); form.resetFields(); }; // 处理编辑 const handleEdit = (record: Gun) => { setIsEdit(true); setCurrentRecord(record); setModalVisible(true); form.setFieldsValue({ ...record, gunNo: record.gunNo, stationPile: [record.stationId, record.pileId] // 设置级联选择器的值 }); }; // 生成充电枪编码 const handleGenerateGunCode = () => { const stationPile = form.getFieldValue('stationPile'); const gunNo = form.getFieldValue('gunNo'); if (!stationPile || stationPile.length !== 2 || !gunNo) { showMessage.warning('请先选择充电站和充电桩,并填写枪号'); return; } const pileId = stationPile[1]; const selectedPile = pileOptions.find(p => p.id === pileId); if (selectedPile) { const code = generateGunCode(selectedPile.pileCode, gunNo); form.setFieldValue('gunCode', code); } }; // 处理表单提交 const handleSubmit = async () => { try { const values = await form.validateFields(); setModalLoading(true); if (isEdit && currentRecord) { // 编辑充电枪 // 从级联选择器值中获取充电站ID和充电桩ID const cascaderValue = values.stationPile; if (!cascaderValue || cascaderValue.length !== 2) { showMessage.error('请选择充电站和充电桩'); return; } const updateData: GunUpdateRequest = { gunName: values.gunName, gunNo: values.gunNo, gunCode: values.gunCode, stationId: cascaderValue[0], // 充电站ID pileId: cascaderValue[1] // 充电桩ID }; await gunService.updateGun(currentRecord.id, updateData); showMessage.success('充电枪更新成功'); } else { // 新建充电枪 // 从级联选择器值中获取充电站ID和充电桩ID const cascaderValue = values.stationPile; if (!cascaderValue || cascaderValue.length !== 2) { showMessage.error('请选择充电站和充电桩'); return; } const createData: GunCreateRequest = { gunName: values.gunName, gunNo: values.gunNo, gunCode: values.gunCode, stationId: cascaderValue[0], // 充电站ID pileId: cascaderValue[1] // 充电桩ID }; await gunService.createGun(createData); showMessage.success('充电枪创建成功'); } setModalVisible(false); // 清空选择状态并重新加载数据 setSelectedRowKeys([]); loadData(); } catch (error: any) { if (error.errorFields) { // 表单验证错误 return; } showMessage.error(getErrorMessage(error)); } finally { setModalLoading(false); } }; // 取消模态框 const handleCancel = () => { setModalVisible(false); form.resetFields(); }; // 处理删除 const handleDelete = async (record: Gun) => { try { console.log('开始删除充电枪:', record.gunName, 'ID:', record.id); await gunService.deleteGun(record.id); console.log('删除充电枪成功:', record.gunName); showMessage.success(`充电枪 "${record.gunName}" 删除成功`); // 清空选择状态并重新加载数据 setSelectedRowKeys([]); loadData(); } catch (error: any) { console.error('删除充电枪失败:', error); console.error('错误详情:', { response: error?.response, data: error?.response?.data, status: error?.response?.status, message: error?.message }); const errorMessage = getErrorMessage(error); console.log('处理后的错误消息:', errorMessage); showMessage.error(`删除充电枪 "${record.gunName}" 失败:${errorMessage}`); } }; // 批量删除 const handleBatchDelete = async () => { if (selectedRowKeys.length === 0) { showMessage.warning('请先选择要删除的记录'); return; } confirm({ title: '确认批量删除', content: (

您确定要删除选中的 {selectedRowKeys.length} 条充电枪吗?

⚠️ 此操作不可撤销,请谨慎操作!

), okText: '确认删除', okType: 'danger', cancelText: '取消', width: 420, centered: true, onOk: async () => { setBatchDeleting(true); let successCount = 0; let failCount = 0; const failedNames: string[] = []; const failedReasons: string[] = []; try { // 使用 for...of 循环按顺序删除 for (const key of selectedRowKeys) { try { await gunService.deleteGun(key as string); successCount++; // 每删除一个都更新进度提示 if (selectedRowKeys.length > 3) { showMessage.loading(`正在删除... (${successCount}/${selectedRowKeys.length})`); } } catch (error: any) { failCount++; const record = dataSource.find(item => item.id === key); const gunName = record?.gunName || `ID: ${key}`; failedNames.push(gunName); // 获取详细错误信息 const errorMessage = getErrorMessage(error); failedReasons.push(`${gunName}: ${errorMessage}`); } } // 显示删除结果 if (failCount === 0) { showMessage.success(`批量删除成功,共删除 ${successCount} 条充电枪`); } else if (successCount === 0) { // 全部失败 showMessage.error( `批量删除失败,所有 ${failCount} 条充电枪都删除失败。失败原因:${failedReasons.join('; ')}` ); } else { // 部分成功 showMessage.warning( `删除完成:成功 ${successCount} 条,失败 ${failCount} 条。失败原因:${failedReasons.join('; ')}` ); } // 重新加载数据并清空选择 setSelectedRowKeys([]); loadData(); } catch (error: any) { // 处理整体操作异常 const errorMessage = getErrorMessage(error); showMessage.error(`批量删除操作失败:${errorMessage}`); } finally { setBatchDeleting(false); } } }); }; // 行选择配置 const rowSelection = { selectedRowKeys, onChange: (newSelectedRowKeys: React.Key[]) => { setSelectedRowKeys(newSelectedRowKeys); }, }; return (

充电枪管理

{selectedRowKeys.length > 0 && ( )}
{/* 搜索表单 */}
{/* 数据表格 */}