添加查询充电枪状态接口

This commit is contained in:
三丙
2025-09-27 18:04:00 +08:00
parent 7a03cc98a7
commit a1e0a09320
74 changed files with 1727 additions and 259 deletions

View File

@@ -17,6 +17,7 @@ import Dashboard from './components/Dashboard';
import StationManagement from './components/StationManagement';
import PileManagement from './components/PileManagement';
import GunManagement from './components/GunManagement';
import GunDebug from './components/GunDebug';
import NotFoundRedirect from './components/NotFoundRedirect';
import './App.css';
@@ -71,6 +72,13 @@ const App: React.FC = () => {
</Layout>
</ProtectedRoute>
} />
<Route path="/page/guns/:gunCode/debug" element={
<ProtectedRoute>
<Layout>
<GunDebug/>
</Layout>
</ProtectedRoute>
}/>
{/* 智能404重定向 - 根据登录状态决定重定向目标 */}
<Route path="*" element={<NotFoundRedirect />} />

View File

@@ -0,0 +1,404 @@
/*
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
import React, {useEffect, useState} from 'react';
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
import {Alert, Breadcrumb, Button, Card, Descriptions, Divider, message, Space, Spin, Typography} from 'antd';
import {ArrowLeftOutlined, BugOutlined, HomeOutlined, PlayCircleOutlined} from '@ant-design/icons';
import {Gun} from '../types';
import * as gunService from '../services/gunService';
import {api, getErrorMessage} from '../services/api';
const {Title, Text} = Typography;
interface DebugResult {
url: string;
method: string;
headers: Record<string, string>;
requestBody: any;
response: any;
timestamp: string;
status: 'success' | 'error';
}
const GunDebug: React.FC = () => {
const {gunCode} = useParams<{ gunCode: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [gun, setGun] = useState<Gun | null>(null);
const [loading, setLoading] = useState(true);
const [debugLoading, setDebugLoading] = useState(false);
const [debugResult, setDebugResult] = useState<DebugResult | null>(null);
const [pileDebugLoading, setPileDebugLoading] = useState(false);
const [pileDebugResult, setPileDebugResult] = useState<DebugResult | null>(null);
// 获取返回URL如果没有则使用默认的充电枪管理页面
const returnUrl = searchParams.get('returnUrl') || '/page/guns';
// 面包屑配置 - 按照官方文档标准写法
const breadcrumbItems = [
{
title: (
<span>
<HomeOutlined/>
<span style={{marginLeft: 8}}></span>
</span>
),
onClick: () => navigate('/page/dashboard'),
},
{
title: '充电枪管理',
onClick: () => navigate(returnUrl),
},
{
title: (
<span>
<BugOutlined/>
<span style={{marginLeft: 8}}></span>
</span>
),
},
];
// 加载充电枪信息
useEffect(() => {
const loadGunInfo = async () => {
if (!gunCode) return;
try {
setLoading(true);
const gun = await gunService.getGunByCode(gunCode);
setGun(gun);
} catch (error: any) {
console.error('加载充电枪信息失败:', error);
const errorMessage = getErrorMessage(error);
message.error(`加载充电枪信息失败: ${errorMessage}`);
navigate('/page/guns');
} finally {
setLoading(false);
}
};
loadGunInfo();
}, [gunCode, navigate]);
// 返回充电枪列表,保持原有的查询参数
const handleBack = () => {
navigate(returnUrl);
};
// 执行充电枪状态查询调试
const handleDebugGunStatus = async () => {
if (!gun) return;
// 清除充电桩调试结果,只显示充电枪调试结果
setPileDebugResult(null);
setDebugLoading(true);
try {
const headers = {
'Content-Type': 'application/json',
};
// 直接调用现有的正确API接口
const response = await api.get(`/api/guns/status/${gun.gunCode}`);
setDebugResult({
url: `/api/guns/status/${gun.gunCode}`,
method: 'GET',
headers,
requestBody: null,
response: response.data,
timestamp: new Date().toLocaleString(),
status: response.data.success ? 'success' : 'error'
});
if (response.data.success) {
message.success('调试执行成功');
} else {
message.error('调试执行失败');
}
} catch (error: any) {
const errorResult = {
url: `/api/guns/status/${gun.gunCode}`,
method: 'GET',
headers: {'Content-Type': 'application/json'},
requestBody: null,
response: {success: false, message: getErrorMessage(error)},
timestamp: new Date().toLocaleString(),
status: 'error' as const
};
setDebugResult(errorResult);
message.error('调试执行失败');
} finally {
setDebugLoading(false);
}
};
// 执行充电桩状态查询调试
const handleDebugPileStatus = async () => {
if (!gun || !gun.pileCode) {
message.error('充电桩编码不存在');
return;
}
// 清除充电枪调试结果,只显示充电桩调试结果
setDebugResult(null);
setPileDebugLoading(true);
try {
const headers = {
'Content-Type': 'application/json',
};
// 直接调用现有的正确API接口
const response = await api.get(`/api/piles/status/${gun.pileCode}`);
setPileDebugResult({
url: `/api/piles/status/${gun.pileCode}`,
method: 'GET',
headers,
requestBody: null,
response: response.data,
timestamp: new Date().toLocaleString(),
status: response.data.success ? 'success' : 'error'
});
if (response.data.success) {
message.success('调试执行成功');
} else {
message.error('调试执行失败');
}
} catch (error: any) {
const errorResult = {
url: `/api/piles/status/${gun.pileCode}`,
method: 'GET',
headers: {'Content-Type': 'application/json'},
requestBody: null,
response: {success: false, message: getErrorMessage(error)},
timestamp: new Date().toLocaleString(),
status: 'error' as const
};
setPileDebugResult(errorResult);
message.error('调试执行失败');
} finally {
setPileDebugLoading(false);
}
};
if (loading) {
return (
<div style={{padding: 24, textAlign: 'center'}}>
<Spin size="large"/>
</div>
);
}
if (!gun) {
return (
<div style={{padding: 24}}>
<Alert
message="充电枪不存在"
description="未找到指定的充电枪信息"
type="error"
showIcon
/>
</div>
);
}
return (
<div style={{padding: 24}}>
{/* 面包屑导航 */}
<Breadcrumb
items={breadcrumbItems}
style={{marginBottom: 16}}
/>
{/* 页面标题和返回按钮 */}
<div style={{marginBottom: 24, display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
<Title level={3} style={{margin: 0}}>
<BugOutlined style={{marginRight: 8}}/>
- {gun.gunName}
</Title>
<Button
icon={<ArrowLeftOutlined/>}
onClick={handleBack}
>
</Button>
</div>
{/* 充电枪基本信息 */}
<Card title="充电枪基本信息" style={{marginBottom: 24}}>
<Descriptions column={2} bordered>
<Descriptions.Item label="充电枪名称">{gun.gunName}</Descriptions.Item>
<Descriptions.Item label="充电枪编码">{gun.gunCode}</Descriptions.Item>
<Descriptions.Item label="充电枪编号">{gun.gunNo}</Descriptions.Item>
<Descriptions.Item label="所属充电桩">{gun.pileName}</Descriptions.Item>
<Descriptions.Item label="充电桩编码">{gun.pileCode}</Descriptions.Item>
<Descriptions.Item label="所属充电站">{gun.stationName}</Descriptions.Item>
<Descriptions.Item label="创建时间">
{gun.createdTime ? new Date(gun.createdTime).toLocaleString() : '-'}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{gun.updatedTime ? new Date(gun.updatedTime).toLocaleString() : '-'}
</Descriptions.Item>
</Descriptions>
</Card>
{/* 调试操作区域 */}
<Card title="调试操作" style={{marginBottom: 24}}>
<Space direction="vertical" style={{width: '100%'}}>
<div>
<Text strong></Text>
<div style={{marginTop: 8}}>
<Space>
<Button
type="primary"
icon={<PlayCircleOutlined/>}
loading={debugLoading}
onClick={handleDebugGunStatus}
>
</Button>
<Button
type="primary"
icon={<PlayCircleOutlined/>}
loading={pileDebugLoading}
onClick={handleDebugPileStatus}
>
</Button>
</Space>
</div>
</div>
</Space>
</Card>
{/* 充电枪调试结果展示 */}
{debugResult && (
<Card title="充电枪状态调试结果">
<Space direction="vertical" style={{width: '100%'}}>
<Alert
message={debugResult.status === 'success' ? '调试成功' : '调试失败'}
type={debugResult.status === 'success' ? 'success' : 'error'}
showIcon
/>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="请求URL">
<Text code>{debugResult.url}</Text>
</Descriptions.Item>
<Descriptions.Item label="请求方法">
<Text code>{debugResult.method}</Text>
</Descriptions.Item>
<Descriptions.Item label="请求时间">
{debugResult.timestamp}
</Descriptions.Item>
</Descriptions>
<Divider orientation="left"> (Headers)</Divider>
<pre style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto'
}}>
{JSON.stringify(debugResult.headers, null, 2)}
</pre>
<Divider orientation="left"> (Request Body)</Divider>
<pre style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto'
}}>
{JSON.stringify(debugResult.requestBody, null, 2)}
</pre>
<Divider orientation="left"> (Response)</Divider>
<pre style={{
background: debugResult.status === 'success' ? '#f6ffed' : '#fff2f0',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto',
border: `1px solid ${debugResult.status === 'success' ? '#b7eb8f' : '#ffccc7'}`
}}>
{JSON.stringify(debugResult.response, null, 2)}
</pre>
</Space>
</Card>
)}
{/* 充电桩调试结果展示 */}
{pileDebugResult && (
<Card title="充电桩状态调试结果" style={{marginTop: 16}}>
<Space direction="vertical" style={{width: '100%'}}>
<Alert
message={pileDebugResult.status === 'success' ? '调试成功' : '调试失败'}
type={pileDebugResult.status === 'success' ? 'success' : 'error'}
showIcon
/>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="请求URL">
<Text code>{pileDebugResult.url}</Text>
</Descriptions.Item>
<Descriptions.Item label="请求方法">
<Text code>{pileDebugResult.method}</Text>
</Descriptions.Item>
<Descriptions.Item label="请求时间">
{pileDebugResult.timestamp}
</Descriptions.Item>
</Descriptions>
<Divider orientation="left"> (Headers)</Divider>
<pre style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto'
}}>
{JSON.stringify(pileDebugResult.headers, null, 2)}
</pre>
<Divider orientation="left"> (Request Body)</Divider>
<pre style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto'
}}>
{JSON.stringify(pileDebugResult.requestBody, null, 2)}
</pre>
<Divider orientation="left"> (Response)</Divider>
<pre style={{
background: pileDebugResult.status === 'success' ? '#f6ffed' : '#fff2f0',
padding: 12,
borderRadius: 4,
fontSize: 12,
overflow: 'auto',
border: `1px solid ${pileDebugResult.status === 'success' ? '#b7eb8f' : '#ffccc7'}`
}}>
{JSON.stringify(pileDebugResult.response, null, 2)}
</pre>
</Space>
</Card>
)}
</div>
);
};
export default GunDebug;

View File

@@ -5,6 +5,7 @@
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
import React, {useEffect, useMemo, useState} from 'react';
import {useLocation, useNavigate, useSearchParams} from 'react-router-dom';
import {
Button,
Card,
@@ -22,7 +23,14 @@ import {
Tag,
Typography
} from 'antd';
import {DeleteOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, TableOutlined} from '@ant-design/icons';
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';
@@ -34,6 +42,9 @@ import type {Gun, GunCreateRequest, GunUpdateRequest, PileOption, StationOption}
const { confirm } = Modal;
const GunManagement: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const [dataSource, setDataSource] = useState<Gun[]>([]);
const [loading, setLoading] = useState(false);
const [searchForm] = Form.useForm();
@@ -55,6 +66,29 @@ const GunManagement: React.FC = () => {
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;
@@ -236,8 +270,13 @@ const GunManagement: React.FC = () => {
<Button type="link" size="small" onClick={() => handleEdit(record)}>
</Button>
<Button type="link" size="small" onClick={() => handleView(record)}>
<Button
type="link"
size="small"
icon={<BugOutlined/>}
onClick={() => handleDebug(record)}
>
</Button>
<Popconfirm
title="确认删除充电枪"
@@ -429,11 +468,22 @@ const GunManagement: React.FC = () => {
}
};
// 监听搜索参数变化
useEffect(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
// 更新搜索参数的函数同时更新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(() => {
@@ -441,6 +491,72 @@ const GunManagement: React.FC = () => {
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<Gun>['onChange'] = (pag, filters, sorter) => {
let newParams = {
@@ -458,7 +574,7 @@ const GunManagement: React.FC = () => {
delete newParams.sortOrder;
}
setSearchParams(newParams);
updateSearchParams(newParams);
};
// 搜索处理
@@ -468,7 +584,7 @@ const GunManagement: React.FC = () => {
size: pagination.pageSize,
...values
};
setSearchParams(newParams);
updateSearchParams(newParams);
};
// 重置搜索
@@ -478,7 +594,7 @@ const GunManagement: React.FC = () => {
page: 1,
size: pagination.pageSize
};
setSearchParams(newParams);
updateSearchParams(newParams);
};
// 显示新建模态框
@@ -500,9 +616,18 @@ const GunManagement: React.FC = () => {
});
};
// 处理查看
const handleView = (record: Gun) => {
showMessage.info('查看功能待实现');
// 处理调试 - 跳转到调试页面,携带当前查询参数
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)}`);
};
// 生成充电枪编码
@@ -971,6 +1096,7 @@ const GunManagement: React.FC = () => {
</Row>
</Form>
</Modal>
</div>
);
};

View File

@@ -5,6 +5,7 @@
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useNavigate} from 'react-router-dom';
import {
Button,
Card,
@@ -44,6 +45,8 @@ const { confirm } = Modal;
const PileManagement: React.FC = () => {
const navigate = useNavigate();
// 状态管理
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<Pile[]>([]);
@@ -95,6 +98,7 @@ const PileManagement: React.FC = () => {
{ key: 'manufacturer', title: '制造商', defaultVisible: false },
{ key: 'type', title: '类型', defaultVisible: false },
{ key: 'status', title: '状态', defaultVisible: true },
{key: 'gunCount', title: '充电枪数量', defaultVisible: true},
{ key: 'connectedAt', title: '连接时间', defaultVisible: true },
{ key: 'disconnectedAt', title: '断线时间', defaultVisible: true },
{ key: 'lastActiveTime', title: '最后活跃时间', defaultVisible: true },
@@ -200,6 +204,27 @@ const PileManagement: React.FC = () => {
)
},
{
title: '充电枪数量',
dataIndex: 'gunCount',
key: 'gunCount',
width: 100,
sorter: true,
render: (gunCount: number, record: Pile) => (
<Button
type="link"
size="small"
onClick={() => handleGunCountClick(record.pileCode)}
style={{
padding: 0,
height: 'auto',
color: gunCount > 0 ? '#1890ff' : '#999'
}}
>
{gunCount || 0}
</Button>
)
},
{
title: '连接时间',
dataIndex: 'connectedAt',
key: 'connectedAt',
@@ -594,6 +619,16 @@ const PileManagement: React.FC = () => {
setModalVisible(true);
};
// 处理充电枪数量点击
const handleGunCountClick = (pileCode: string) => {
// 跳转到充电枪管理页面,并设置搜索条件
navigate('/page/guns', {
state: {
searchPileCode: pileCode
}
});
};
// 生成充电桩编码
const handleGeneratePileCode = () => {

View File

@@ -31,3 +31,8 @@ export const getGun = async (id: string): Promise<Gun> => {
return response.data.data;
};
export const getGunByCode = async (gunCode: string): Promise<Gun> => {
const response = await api.get(`/api/guns/code/${gunCode}`);
return response.data.data;
};

View File

@@ -45,6 +45,7 @@ export interface Pile {
connectedAt?: number;
disconnectedAt?: number;
lastActiveTime?: number;
gunCount?: number;
createdTime: number;
updatedTime?: number;
}