Files
ems-core/frontend/src/pages/Charging/Piles.tsx

204 lines
8.1 KiB
TypeScript
Raw Normal View History

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>
);
}