/* * 开源代码,仅供学习和交流研究使用,商用请联系三丙 * 微信:mohan_88888 * 抖音:程序员三丙 * 付费课程知识星球:https://t.zsxq.com/aKtXo */ import React, {useEffect, useMemo, useState} from 'react'; import {useLocation, useNavigate, useSearchParams} from 'react-router-dom'; import { Button, Card, Checkbox, Col, Dropdown, Form, Input, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd'; import { BugOutlined, 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 location = useLocation(); const navigate = useNavigate(); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); 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 [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} 条记录` }); // 从URL参数初始化搜索参数 const initSearchParams = (): { page: number; size: number; gunName?: string; gunCode?: string; gunNo?: string; stationId?: string; sortField?: string; sortOrder?: string; } => { return { page: parseInt(urlSearchParams.get('page') || '1'), size: parseInt(urlSearchParams.get('size') || '10'), gunName: urlSearchParams.get('gunName') || undefined, gunCode: urlSearchParams.get('gunCode') || undefined, gunNo: urlSearchParams.get('gunNo') || undefined, stationId: urlSearchParams.get('stationId') || undefined, sortField: urlSearchParams.get('sortField') || undefined, sortOrder: urlSearchParams.get('sortOrder') || undefined, }; }; 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); } }; // 更新搜索参数的函数,同时更新URL const updateSearchParams = (newParams: any) => { setSearchParams(newParams); // 更新URL参数 const urlParams = new URLSearchParams(); Object.entries(newParams).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { urlParams.set(key, String(value)); } }); setUrlSearchParams(urlParams); }; // 标记是否已经初始化URL参数 const [urlParamsInitialized, setUrlParamsInitialized] = useState(false); // 初始化加载充电站选项和充电桩选项 useEffect(() => { loadStationOptions(); loadPileOptions(); }, []); // 组件挂载后立即从URL参数初始化搜索参数 useEffect(() => { const urlParams = initSearchParams(); // 只有当URL参数与当前searchParams不同时才更新 const hasUrlParams = urlParams.gunName || urlParams.gunCode || urlParams.gunNo || urlParams.stationId || urlParams.sortField || urlParams.sortOrder || urlParams.page !== 1 || urlParams.size !== 10; if (hasUrlParams) { console.log('从URL初始化搜索参数:', urlParams); setSearchParams(urlParams); } setUrlParamsInitialized(true); }, [urlSearchParams]); // 依赖urlSearchParams,确保URL变化时重新初始化 // 监听搜索参数变化,但只有在URL参数初始化完成后才加载数据 useEffect(() => { if (urlParamsInitialized) { loadData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams, urlParamsInitialized]); // 初始化表单值(从URL参数) useEffect(() => { const initialValues = { gunName: searchParams.gunName || '', gunCode: searchParams.gunCode || '', gunNo: searchParams.gunNo || '', stationId: searchParams.stationId || '', }; searchForm.setFieldsValue(initialValues); }, [searchForm, searchParams.gunName, searchParams.gunCode, searchParams.gunNo, searchParams.stationId]); // 初始化时如果URL有搜索参数,需要触发一次数据加载 useEffect(() => { // 检查是否有搜索条件(除了page和size之外的参数) const hasSearchConditions = searchParams.gunName || searchParams.gunCode || searchParams.gunNo || searchParams.stationId; if (hasSearchConditions) { // 如果有搜索条件,确保数据会被重新加载 // 这里不需要手动调用loadData,因为searchParams的变化会触发useEffect中的loadData console.log('检测到URL搜索参数,将自动加载数据:', searchParams); } }, []); // 只在组件初始化时执行一次 // 处理从充电桩管理页面传来的搜索参数 useEffect(() => { const state = location.state as { searchPileCode?: string } | null; if (state?.searchPileCode) { // 设置搜索表单的值 searchForm.setFieldValue('gunCode', state.searchPileCode); // 更新搜索参数并触发搜索 updateSearchParams({ ...searchParams, gunCode: state.searchPileCode, page: 1 // 重置到第一页 }); // 清除location.state,避免重复处理 window.history.replaceState({}, document.title); } }, [location.state]); // 处理表格变化 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; } updateSearchParams(newParams); }; // 搜索处理 const handleSearch = (values: any) => { const newParams = { page: 1, size: pagination.pageSize, ...values }; updateSearchParams(newParams); }; // 重置搜索 const handleReset = () => { searchForm.resetFields(); const newParams = { page: 1, size: pagination.pageSize }; updateSearchParams(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.toString() }); }; // 处理调试 - 跳转到调试页面,携带当前查询参数 const handleDebug = (record: Gun) => { // 直接从URL中获取当前的查询参数,确保获取到最新的参数 const currentUrlParams = new URLSearchParams(window.location.search); const queryString = currentUrlParams.toString(); const returnUrl = queryString ? `/page/guns?${queryString}` : '/page/guns'; console.log('调试跳转 - 当前URL参数:', queryString); console.log('调试跳转 - 返回URL:', returnUrl); // 将returnUrl作为URL参数传递 navigate(`/page/guns/${record.gunCode}/debug?returnUrl=${encodeURIComponent(returnUrl)}`); }; // 生成充电枪编码 const handleGenerateGunCode = () => { const pileId = form.getFieldValue('pileId'); const gunNo = form.getFieldValue('gunNo'); if (!pileId || !gunNo) { showMessage.warning('请先选择充电桩和填写枪号'); return; } 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) { // 编辑充电枪 const updateData: GunUpdateRequest = { gunName: values.gunName, gunNo: values.gunNo, gunCode: values.gunCode, stationId: values.stationId, pileId: values.pileId }; await gunService.updateGun(currentRecord.id, updateData); showMessage.success('充电枪更新成功'); } else { // 新建充电枪 const createData: GunCreateRequest = { gunName: values.gunName, gunNo: values.gunNo, gunCode: values.gunCode, stationId: values.stationId, pileId: values.pileId }; 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 && ( )}
{/* 搜索表单 */}
{/* 数据表格 */}