ems-core v1.0.0: Standard EMS platform core
Shared backend + frontend for multi-customer EMS deployments. - 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc. - 120+ API endpoints, 37 database tables - Customer config mechanism (CUSTOMER env var + YAML config) - Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud - Frontend: React 19 + Ant Design + ECharts + Three.js - Infrastructure: Redis cache, rate limiting, aggregation engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
203
frontend/src/pages/Charging/Piles.tsx
Normal file
203
frontend/src/pages/Charging/Piles.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Card, Table, Tag, Button, Modal, Form, Input, Select, InputNumber, Space, message } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { getChargingPiles, createChargingPile, updateChargingPile, deleteChargingPile, getChargingStations, getChargingBrands } from '../../services/api';
|
||||
|
||||
const workStatusMap: Record<string, { color: string; text: string }> = {
|
||||
idle: { color: 'green', text: '空闲' },
|
||||
charging: { color: 'blue', text: '充电中' },
|
||||
fault: { color: 'red', text: '故障' },
|
||||
offline: { color: 'default', text: '离线' },
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '交流慢充', value: 'AC_slow' },
|
||||
{ label: '直流快充', value: 'DC_fast' },
|
||||
{ label: '直流超充', value: 'DC_superfast' },
|
||||
];
|
||||
|
||||
const connectorOptions = [
|
||||
{ label: 'GB/T', value: 'GB_T' },
|
||||
{ label: 'CCS', value: 'CCS' },
|
||||
{ label: 'CHAdeMO', value: 'CHAdeMO' },
|
||||
];
|
||||
|
||||
export default function Piles() {
|
||||
const [data, setData] = useState<any>({ total: 0, items: [] });
|
||||
const [stations, setStations] = useState<any[]>([]);
|
||||
const [brands, setBrands] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const cleanQuery: Record<string, any> = {};
|
||||
Object.entries(filters).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') cleanQuery[k] = v;
|
||||
});
|
||||
const res = await getChargingPiles(cleanQuery);
|
||||
setData(res as any);
|
||||
} catch { message.error('加载充电桩失败'); }
|
||||
finally { setLoading(false); }
|
||||
}, [filters]);
|
||||
|
||||
const loadMeta = async () => {
|
||||
try {
|
||||
const [st, br] = await Promise.all([
|
||||
getChargingStations({ page_size: 100 }),
|
||||
getChargingBrands(),
|
||||
]);
|
||||
setStations((st as any).items || []);
|
||||
setBrands(br as any[]);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
useEffect(() => { loadMeta(); }, []);
|
||||
useEffect(() => { loadData(); }, [filters, loadData]);
|
||||
|
||||
const handleFilterChange = (key: string, value: any) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value, page: 1 }));
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ status: 'active', work_status: 'offline' });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const openEditModal = (record: any) => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue(record);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
if (editing) {
|
||||
await updateChargingPile(editing.id, values);
|
||||
message.success('充电桩更新成功');
|
||||
} else {
|
||||
await createChargingPile(values);
|
||||
message.success('充电桩创建成功');
|
||||
}
|
||||
setShowModal(false);
|
||||
form.resetFields();
|
||||
loadData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.detail || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteChargingPile(id);
|
||||
message.success('已停用');
|
||||
loadData();
|
||||
} catch { message.error('操作失败'); }
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '终端编码', dataIndex: 'encoding', width: 140 },
|
||||
{ title: '名称', dataIndex: 'name', width: 150, ellipsis: true },
|
||||
{ title: '所属充电站', dataIndex: 'station_id', width: 150, render: (id: number) => {
|
||||
const s = stations.find((st: any) => st.id === id);
|
||||
return s ? s.name : id;
|
||||
}},
|
||||
{ title: '类型', dataIndex: 'type', width: 100, render: (v: string) => typeOptions.find(o => o.value === v)?.label || v || '-' },
|
||||
{ title: '额定功率(kW)', dataIndex: 'rated_power_kw', width: 120, render: (v: number) => v != null ? v : '-' },
|
||||
{ title: '品牌', dataIndex: 'brand', width: 100 },
|
||||
{ title: '型号', dataIndex: 'model', width: 100 },
|
||||
{ title: '接口类型', dataIndex: 'connector_type', width: 100 },
|
||||
{ title: '工作状态', dataIndex: 'work_status', width: 100, render: (s: string) => {
|
||||
const st = workStatusMap[s] || { color: 'default', text: s || '-' };
|
||||
return <Tag color={st.color}>{st.text}</Tag>;
|
||||
}},
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => (
|
||||
<Tag color={s === 'active' ? 'green' : 'default'}>{s === 'active' ? '启用' : '停用'}</Tag>
|
||||
)},
|
||||
{ title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => openEditModal(record)}>编辑</Button>
|
||||
<Button size="small" type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>停用</Button>
|
||||
</Space>
|
||||
)},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card size="small" title="充电桩管理" extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddModal}>添加充电桩</Button>
|
||||
}>
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Select allowClear placeholder="所属充电站" style={{ width: 180 }}
|
||||
options={stations.map((s: any) => ({ label: s.name, value: s.id }))}
|
||||
onChange={v => handleFilterChange('station_id', v)} />
|
||||
<Select allowClear placeholder="类型" style={{ width: 120 }} options={typeOptions}
|
||||
onChange={v => handleFilterChange('type', v)} />
|
||||
<Select allowClear placeholder="工作状态" style={{ width: 120 }}
|
||||
options={[
|
||||
{ label: '空闲', value: 'idle' }, { label: '充电中', value: 'charging' },
|
||||
{ label: '故障', value: 'fault' }, { label: '离线', value: 'offline' },
|
||||
]}
|
||||
onChange={v => handleFilterChange('work_status', v)} />
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns} dataSource={data.items} rowKey="id"
|
||||
loading={loading} size="small" scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
current: filters.page,
|
||||
pageSize: filters.page_size,
|
||||
total: data.total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 个充电桩`,
|
||||
onChange: (page, pageSize) => setFilters(prev => ({ ...prev, page, page_size: pageSize })),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑充电桩' : '添加充电桩'}
|
||||
open={showModal}
|
||||
onCancel={() => { setShowModal(false); form.resetFields(); }}
|
||||
onOk={() => form.submit()}
|
||||
okText={editing ? '保存' : '创建'}
|
||||
cancelText="取消"
|
||||
width={640}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item name="station_id" label="所属充电站" rules={[{ required: true, message: '请选择充电站' }]}>
|
||||
<Select placeholder="选择充电站"
|
||||
options={stations.map((s: any) => ({ label: s.name, value: s.id }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="encoding" label="终端编码" rules={[{ required: true, message: '请输入终端编码' }]}>
|
||||
<Input placeholder="唯一终端编码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="名称">
|
||||
<Input placeholder="充电桩名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="type" label="类型">
|
||||
<Select allowClear placeholder="选择类型" options={typeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="brand" label="品牌">
|
||||
<Select allowClear placeholder="选择品牌"
|
||||
options={brands.map((b: any) => ({ label: b.brand_name, value: b.brand_name }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="model" label="型号">
|
||||
<Input placeholder="设备型号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="rated_power_kw" label="额定功率(kW)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="connector_type" label="接口类型">
|
||||
<Select allowClear placeholder="选择接口类型" options={connectorOptions} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user