feat: enterprise-level enhancement — 12 modules complete

New modules:
- Energy Quota Management (定额管理)
- Cost/Expense Analysis with TOU pricing (费用分析)
- Sub-item Energy Analysis (分项分析)
- EV Charging Station Management (充电桩管理) — 8 models, 6 pages
- Enhanced Energy Analysis — loss, YoY, MoM comparison
- Alarm Analytics — trends, MTTR, top devices, rule toggle
- Maintenance & Work Orders (运维管理) — inspections, repair orders, duty
- Data Query Module (数据查询)
- Equipment Topology (设备拓扑)
- Management System (管理体系) — regulations, standards, processes

Infrastructure:
- Redis caching layer with decorator
- Redis Streams data ingestion buffer
- Hourly/daily/monthly aggregation engine
- Rate limiting & request ID middleware
- 6 Alembic migrations (003-008), 21 new tables
- Extended seed data for all modules

Stats: 120+ API routes, 12 pages, 27 tabs, 37 database tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-03 22:06:16 +08:00
parent 38b28bb8b3
commit e2b7421bc4
62 changed files with 9422 additions and 22 deletions

View File

@@ -16,6 +16,11 @@ import Reports from './pages/Reports';
import Devices from './pages/Devices';
import DeviceDetail from './pages/DeviceDetail';
import SystemManagement from './pages/System';
import Quota from './pages/Quota';
import Charging from './pages/Charging';
import Maintenance from './pages/Maintenance';
import DataQuery from './pages/DataQuery';
import Management from './pages/Management';
import BigScreen from './pages/BigScreen';
import BigScreen3D from './pages/BigScreen3D';
import { isLoggedIn } from './utils/auth';
@@ -51,6 +56,11 @@ function AppContent() {
<Route path="alarms" element={<Alarms />} />
<Route path="carbon" element={<Carbon />} />
<Route path="reports" element={<Reports />} />
<Route path="quota" element={<Quota />} />
<Route path="charging/*" element={<Charging />} />
<Route path="maintenance" element={<Maintenance />} />
<Route path="data-query" element={<DataQuery />} />
<Route path="management" element={<Management />} />
<Route path="system/*" element={<SystemManagement />} />
</Route>
</Routes>

View File

@@ -14,7 +14,12 @@
"users": "User Management",
"roles": "Roles & Permissions",
"settings": "System Settings",
"audit": "Audit Log"
"audit": "Audit Log",
"quota": "Quota Management",
"charging": "Charging Management",
"maintenance": "O&M Management",
"dataQuery": "Data Query",
"management": "Management System"
},
"header": {
"alarmNotification": "Alarm Notifications",

View File

@@ -14,7 +14,12 @@
"users": "用户管理",
"roles": "角色权限",
"settings": "系统设置",
"audit": "审计日志"
"audit": "审计日志",
"quota": "定额管理",
"charging": "充电管理",
"maintenance": "运维管理",
"dataQuery": "数据查询",
"management": "管理体系"
},
"header": {
"alarmNotification": "告警通知",

View File

@@ -6,7 +6,8 @@ import {
MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, BellOutlined,
ThunderboltOutlined, AppstoreOutlined, WarningOutlined, CloseCircleOutlined,
InfoCircleOutlined, FundProjectionScreenOutlined, GlobalOutlined,
BulbOutlined, BulbFilled,
BulbOutlined, BulbFilled, FundOutlined, CarOutlined, ToolOutlined,
SearchOutlined, SolutionOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -41,6 +42,11 @@ export default function MainLayout() {
{ key: '/alarms', icon: <AlertOutlined />, label: t('menu.alarms') },
{ key: '/carbon', icon: <CloudOutlined />, label: t('menu.carbon') },
{ key: '/reports', icon: <FileTextOutlined />, label: t('menu.reports') },
{ key: '/quota', icon: <FundOutlined />, label: t('menu.quota', '定额管理') },
{ key: '/charging', icon: <CarOutlined />, label: t('menu.charging', '充电管理') },
{ key: '/maintenance', icon: <ToolOutlined />, label: t('menu.maintenance', '运维管理') },
{ key: '/data-query', icon: <SearchOutlined />, label: t('menu.dataQuery', '数据查询') },
{ key: '/management', icon: <SolutionOutlined />, label: t('menu.management', '管理体系') },
{ key: 'bigscreen-group', icon: <FundProjectionScreenOutlined />, label: t('menu.bigscreen'),
children: [
{ key: '/bigscreen', icon: <FundProjectionScreenOutlined />, label: t('menu.bigscreen2d') },

View File

@@ -1,7 +1,11 @@
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber, Space, message } from 'antd';
import { PlusOutlined, CheckOutlined, ToolOutlined } from '@ant-design/icons';
import { getAlarmEvents, getAlarmRules, createAlarmRule, acknowledgeAlarm, resolveAlarm } from '../../services/api';
import { Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber, Space, Switch, Drawer, Row, Col, Statistic, message } from 'antd';
import { PlusOutlined, CheckOutlined, ToolOutlined, HistoryOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import {
getAlarmEvents, getAlarmRules, createAlarmRule, acknowledgeAlarm, resolveAlarm,
getAlarmAnalytics, getTopAlarmDevices, getAlarmMttr, toggleAlarmRule, getAlarmRuleHistory,
} from '../../services/api';
const severityMap: Record<string, { color: string; text: string }> = {
critical: { color: 'red', text: '紧急' },
@@ -15,12 +19,122 @@ const statusMap: Record<string, { color: string; text: string }> = {
resolved: { color: 'green', text: '已解决' },
};
function AlarmAnalyticsTab() {
const [analytics, setAnalytics] = useState<any>(null);
const [topDevices, setTopDevices] = useState<any[]>([]);
const [mttr, setMttr] = useState<any>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
loadAnalytics();
}, []);
const loadAnalytics = async () => {
setLoading(true);
try {
const [ana, top, mt] = await Promise.all([
getAlarmAnalytics({}),
getTopAlarmDevices({}),
getAlarmMttr({}),
]);
setAnalytics(ana);
setTopDevices(top as any[]);
setMttr(mt);
} catch {
message.error('加载告警分析数据失败');
} finally {
setLoading(false);
}
};
const trendOption = analytics ? {
tooltip: { trigger: 'axis' },
legend: { data: ['紧急', '重要', '一般'] },
grid: { top: 50, right: 40, bottom: 30, left: 60 },
xAxis: { type: 'category', data: analytics.daily_trend.map((d: any) => d.date) },
yAxis: { type: 'value', name: '次数' },
series: [
{ name: '紧急', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.critical), lineStyle: { color: '#f5222d' }, itemStyle: { color: '#f5222d' } },
{ name: '重要', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.major), lineStyle: { color: '#fa8c16' }, itemStyle: { color: '#fa8c16' } },
{ name: '一般', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.warning), lineStyle: { color: '#fadb14' }, itemStyle: { color: '#fadb14' } },
],
} : {};
const topDevicesOption = {
tooltip: { trigger: 'axis' },
grid: { top: 20, right: 40, bottom: 30, left: 120 },
xAxis: { type: 'value', name: '告警次数' },
yAxis: { type: 'category', data: [...topDevices].reverse().map(d => d.device_name) },
series: [{
type: 'bar',
data: [...topDevices].reverse().map(d => d.alarm_count),
itemStyle: { color: '#fa8c16' },
}],
};
const totals = analytics?.totals || {};
const pieOption = {
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: [
{ value: totals.critical || 0, name: '紧急', itemStyle: { color: '#f5222d' } },
{ value: totals.major || 0, name: '重要', itemStyle: { color: '#fa8c16' } },
{ value: totals.warning || 0, name: '一般', itemStyle: { color: '#fadb14' } },
],
}],
};
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
{(['critical', 'major', 'warning'] as const).map(sev => (
<Col xs={24} sm={8} key={sev}>
<Card>
<Statistic
title={`${severityMap[sev].text} MTTR`}
value={mttr[sev]?.avg_hours || 0}
suffix="小时"
precision={1}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{mttr[sev]?.count || 0}
</div>
</Card>
</Col>
))}
</Row>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} lg={16}>
<Card title="告警趋势 (近30天)" size="small" loading={loading}>
{analytics && <ReactECharts option={trendOption} style={{ height: 300 }} />}
</Card>
</Col>
<Col xs={24} lg={8}>
<Card title="告警类型分布" size="small" loading={loading}>
<ReactECharts option={pieOption} style={{ height: 300 }} />
</Card>
</Col>
</Row>
<Card title="告警设备 Top 10" size="small" loading={loading}>
<ReactECharts option={topDevicesOption} style={{ height: 350 }} />
</Card>
</div>
);
}
export default function Alarms() {
const [events, setEvents] = useState<any>({ total: 0, items: [] });
const [rules, setRules] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showRuleModal, setShowRuleModal] = useState(false);
const [form] = Form.useForm();
const [historyDrawer, setHistoryDrawer] = useState<{ visible: boolean; ruleId: number; ruleName: string }>({ visible: false, ruleId: 0, ruleName: '' });
const [historyData, setHistoryData] = useState<any>({ total: 0, items: [] });
const [historyLoading, setHistoryLoading] = useState(false);
useEffect(() => { loadData(); }, []);
@@ -60,6 +174,24 @@ export default function Alarms() {
} catch { message.error('规则创建失败'); }
};
const handleToggleRule = async (ruleId: number) => {
try {
await toggleAlarmRule(ruleId);
message.success('状态已更新');
loadData();
} catch { message.error('切换状态失败'); }
};
const handleShowHistory = async (ruleId: number, ruleName: string) => {
setHistoryDrawer({ visible: true, ruleId, ruleName });
setHistoryLoading(true);
try {
const res = await getAlarmRuleHistory(ruleId, { page: 1, page_size: 20 });
setHistoryData(res);
} catch { message.error('加载规则历史失败'); }
finally { setHistoryLoading(false); }
};
const eventColumns = [
{ title: '级别', dataIndex: 'severity', width: 80, render: (s: string) => {
const sv = severityMap[s] || { color: 'default', text: s };
@@ -87,7 +219,31 @@ export default function Alarms() {
{ title: '条件', dataIndex: 'condition' },
{ title: '阈值', dataIndex: 'threshold' },
{ title: '级别', dataIndex: 'severity', render: (s: string) => <Tag color={severityMap[s]?.color}>{severityMap[s]?.text}</Tag> },
{ title: '状态', dataIndex: 'is_active', render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag> },
{
title: '启用',
dataIndex: 'is_active',
width: 80,
render: (v: boolean, r: any) => (
<Switch checked={v} onChange={() => handleToggleRule(r.id)} size="small" />
),
},
{
title: '操作',
key: 'action',
width: 100,
render: (_: any, r: any) => (
<Button size="small" icon={<HistoryOutlined />} onClick={() => handleShowHistory(r.id, r.name)}></Button>
),
},
];
const historyColumns = [
{ title: '级别', dataIndex: 'severity', width: 80, render: (s: string) => <Tag color={severityMap[s]?.color}>{severityMap[s]?.text}</Tag> },
{ title: '告警标题', dataIndex: 'title' },
{ title: '触发值', dataIndex: 'value', render: (v: number) => v?.toFixed(2) },
{ title: '状态', dataIndex: 'status', render: (s: string) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.text}</Tag> },
{ title: '触发时间', dataIndex: 'triggered_at', width: 180 },
{ title: '解决时间', dataIndex: 'resolved_at', width: 180 },
];
return (
@@ -106,6 +262,7 @@ export default function Alarms() {
loading={loading} size="small" />
</Card>
)},
{ key: 'analytics', label: '分析', children: <AlarmAnalyticsTab /> },
]} />
<Modal title="新建告警规则" open={showRuleModal} onCancel={() => setShowRuleModal(false)}
@@ -141,6 +298,17 @@ export default function Alarms() {
</Form.Item>
</Form>
</Modal>
<Drawer
title={`规则触发历史 - ${historyDrawer.ruleName}`}
open={historyDrawer.visible}
onClose={() => setHistoryDrawer({ visible: false, ruleId: 0, ruleName: '' })}
width={700}
>
<Table columns={historyColumns} dataSource={historyData.items} rowKey="id"
loading={historyLoading} size="small"
pagination={{ total: historyData.total, pageSize: 20 }} />
</Drawer>
</div>
);
}

View File

@@ -0,0 +1,245 @@
import { useEffect, useState } from 'react';
import { Card, Row, Col, DatePicker, Select, Statistic, Button, Space, message } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined, DownloadOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import dayjs, { type Dayjs } from 'dayjs';
import { getCostSummary, getCostComparison, getCostBreakdown } from '../../services/api';
const { RangePicker } = DatePicker;
export default function CostAnalysis() {
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs().subtract(30, 'day'), dayjs(),
]);
const [groupBy, setGroupBy] = useState('day');
const [comparison, setComparison] = useState<any>(null);
const [summary, setSummary] = useState<any[]>([]);
const [breakdown, setBreakdown] = useState<any>(null);
const [loading, setLoading] = useState(false);
const loadData = async () => {
setLoading(true);
try {
const start = dateRange[0].format('YYYY-MM-DD');
const end = dateRange[1].format('YYYY-MM-DD');
const [comp, sum, bkd] = await Promise.all([
getCostComparison({ energy_type: 'electricity', period: 'month' }),
getCostSummary({ start_date: start, end_date: end, group_by: groupBy, energy_type: 'electricity' }),
getCostBreakdown({ start_date: start, end_date: end, energy_type: 'electricity' }),
]);
setComparison(comp);
setSummary(sum as any[]);
setBreakdown(bkd);
} catch (e) {
console.error(e);
message.error('加载费用数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [groupBy]);
// KPI calculations
const todayCost = comparison?.current || 0;
const monthCost = comparison?.current || 0;
const yearCost = comparison?.yoy || 0;
const momChange = comparison?.mom_change || 0;
const yoyChange = comparison?.yoy_change || 0;
// Breakdown pie chart
const breakdownPieOption = {
tooltip: { trigger: 'item', formatter: '{b}: {c} 元 ({d}%)' },
legend: { bottom: 10 },
series: [{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{d}%' },
data: (breakdown?.periods || []).map((p: any) => ({
value: p.cost,
name: p.period_label || p.period_name,
itemStyle: {
color: p.period_name === 'peak' || p.period_name === 'sharp' ? '#f5222d'
: p.period_name === 'valley' || p.period_name === 'off_peak' ? '#52c41a'
: p.period_name === 'flat' ? '#1890ff' : '#faad14',
},
})),
}],
};
// Cost trend line chart
const trendChartOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['费用(元)', '用电量(kWh)'] },
grid: { top: 50, right: 60, bottom: 30, left: 60 },
xAxis: {
type: 'category',
data: summary.map((d: any) => {
if (d.date) return dayjs(d.date).format('MM/DD');
if (d.period) return d.period;
if (d.device_name) return d.device_name;
return '';
}),
},
yAxis: [
{ type: 'value', name: '元', position: 'left' },
{ type: 'value', name: 'kWh', position: 'right' },
],
series: [
{
name: '费用(元)',
type: groupBy === 'device' ? 'bar' : 'line',
smooth: true,
data: summary.map((d: any) => d.cost || 0),
lineStyle: { color: '#f5222d' },
itemStyle: { color: '#f5222d' },
yAxisIndex: 0,
},
{
name: '用电量(kWh)',
type: groupBy === 'device' ? 'bar' : 'line',
smooth: true,
data: summary.map((d: any) => d.consumption || 0),
lineStyle: { color: '#1890ff' },
itemStyle: { color: '#1890ff' },
yAxisIndex: 1,
},
],
};
// Cost by building bar chart (using device grouping)
const [deviceSummary, setDeviceSummary] = useState<any[]>([]);
useEffect(() => {
const loadDeviceSummary = async () => {
try {
const start = dateRange[0].format('YYYY-MM-DD');
const end = dateRange[1].format('YYYY-MM-DD');
const data = await getCostSummary({
start_date: start, end_date: end, group_by: 'device', energy_type: 'electricity',
});
setDeviceSummary(data as any[]);
} catch (e) {
console.error(e);
}
};
loadDeviceSummary();
}, [dateRange]);
const deviceBarOption = {
tooltip: { trigger: 'axis' },
grid: { top: 30, right: 20, bottom: 60, left: 60 },
xAxis: {
type: 'category',
data: deviceSummary.map((d: any) => d.device_name || `#${d.device_id}`),
axisLabel: { rotate: 30, fontSize: 11 },
},
yAxis: { type: 'value', name: '元' },
series: [{
type: 'bar',
data: deviceSummary.map((d: any) => d.cost || 0),
itemStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#1890ff' },
{ offset: 1, color: '#69c0ff' },
],
},
},
barMaxWidth: 40,
}],
};
const handleExport = () => {
const rows = summary.map((d: any) => {
const label = d.date || d.period || d.device_name || '';
return `${label},${d.consumption || 0},${d.cost || 0}`;
});
const csv = '\ufeff日期/分组,用电量(kWh),费用(元)\n' + rows.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `cost_analysis_${dateRange[0].format('YYYYMMDD')}_${dateRange[1].format('YYYYMMDD')}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
message.success('导出成功');
};
return (
<div>
{/* Controls */}
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<RangePicker
value={dateRange}
onChange={(dates) => dates && setDateRange(dates as [Dayjs, Dayjs])}
/>
<Select value={groupBy} onChange={setGroupBy} style={{ width: 120 }}
options={[
{ label: '按天', value: 'day' },
{ label: '按月', value: 'month' },
{ label: '按设备', value: 'device' },
]}
/>
<Button type="primary" loading={loading} onClick={loadData}></Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
</Space>
</Card>
{/* KPI Cards */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="今日费用" value={todayCost} suffix="元" precision={2}
valueStyle={{ color: '#f5222d' }} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="本月费用" value={monthCost} suffix="元" precision={2} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="环比变化" value={Math.abs(momChange)} suffix="%"
prefix={momChange >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: momChange >= 0 ? '#f5222d' : '#52c41a' }} precision={1} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="同比变化" value={Math.abs(yoyChange)} suffix="%"
prefix={yoyChange >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: yoyChange >= 0 ? '#f5222d' : '#52c41a' }} precision={1} />
</Card>
</Col>
</Row>
{/* Charts Row */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} md={10}>
<Card title="分时电价费用分布" size="small">
<ReactECharts option={breakdownPieOption} style={{ height: 320 }} />
</Card>
</Col>
<Col xs={24} md={14}>
<Card title="费用趋势" size="small">
<ReactECharts option={trendChartOption} style={{ height: 320 }} />
</Card>
</Col>
</Row>
{/* Device cost bar chart */}
<Card title="设备/区域费用分布" size="small">
<ReactECharts option={deviceBarOption} style={{ height: 350 }} />
</Card>
</div>
);
}

View File

@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react';
import { Card, Table, DatePicker, Select, Space, Tag, message } from 'antd';
import ReactECharts from 'echarts-for-react';
import dayjs, { type Dayjs } from 'dayjs';
import { getEnergyLoss } from '../../services/api';
const { RangePicker } = DatePicker;
interface LossItem {
group_name: string;
parent_consumption: number;
children_consumption: number;
loss: number;
loss_rate_pct: number;
}
export default function LossAnalysis() {
const [data, setData] = useState<LossItem[]>([]);
const [loading, setLoading] = useState(false);
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs().subtract(30, 'day'), dayjs(),
]);
const [energyType, setEnergyType] = useState('electricity');
const loadData = async () => {
setLoading(true);
try {
const res = await getEnergyLoss({
start_date: dateRange[0].format('YYYY-MM-DD'),
end_date: dateRange[1].format('YYYY-MM-DD'),
energy_type: energyType,
});
setData(res as LossItem[]);
} catch {
message.error('加载损耗数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => { loadData(); }, [dateRange, energyType]);
const getLossColor = (rate: number) => {
if (rate > 10) return 'red';
if (rate >= 5) return 'gold';
return 'green';
};
const columns = [
{ title: '区域', dataIndex: 'group_name' },
{ title: '供给量', dataIndex: 'parent_consumption', render: (v: number) => `${v.toFixed(2)} kWh` },
{ title: '消耗量', dataIndex: 'children_consumption', render: (v: number) => `${v.toFixed(2)} kWh` },
{ title: '损耗量', dataIndex: 'loss', render: (v: number) => `${v.toFixed(2)} kWh` },
{
title: '损耗率',
dataIndex: 'loss_rate_pct',
render: (v: number) => <Tag color={getLossColor(v)}>{v.toFixed(1)}%</Tag>,
sorter: (a: LossItem, b: LossItem) => a.loss_rate_pct - b.loss_rate_pct,
},
];
const chartOption = {
tooltip: { trigger: 'axis' },
grid: { top: 40, right: 40, bottom: 30, left: 80 },
xAxis: { type: 'value', name: 'kWh' },
yAxis: { type: 'category', data: data.map(d => d.group_name) },
series: [
{
name: '损耗量',
type: 'bar',
data: data.map(d => ({
value: d.loss,
itemStyle: { color: d.loss_rate_pct > 10 ? '#f5222d' : d.loss_rate_pct >= 5 ? '#faad14' : '#52c41a' },
})),
},
],
};
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<span>:</span>
<RangePicker value={dateRange} onChange={(dates) => dates && setDateRange(dates as [Dayjs, Dayjs])} />
<span>:</span>
<Select value={energyType} onChange={setEnergyType} style={{ width: 120 }}
options={[
{ label: '电力', value: 'electricity' },
{ label: '热力', value: 'heat' },
{ label: '水', value: 'water' },
{ label: '燃气', value: 'gas' },
]}
/>
</Space>
</Card>
<Card title="损耗分布" size="small" style={{ marginBottom: 16 }}>
<ReactECharts option={chartOption} style={{ height: 300 }} />
</Card>
<Card title="损耗明细" size="small">
<Table columns={columns} dataSource={data} rowKey="group_name"
loading={loading} size="small" pagination={false} />
</Card>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import { useEffect, useState } from 'react';
import { Card, Row, Col, Select, Space, Statistic, message } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import { getEnergyMom } from '../../services/api';
interface MomItem {
label: string;
current_period: number;
previous_period: number;
change_pct: number;
}
interface MomData {
items: MomItem[];
total_current: number;
total_previous: number;
total_change_pct: number;
}
export default function MomAnalysis() {
const [data, setData] = useState<MomData | null>(null);
const [loading, setLoading] = useState(false);
const [period, setPeriod] = useState('month');
const [energyType, setEnergyType] = useState('electricity');
const loadData = async () => {
setLoading(true);
try {
const res = await getEnergyMom({ period, energy_type: energyType });
setData(res as MomData);
} catch {
message.error('加载环比数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => { loadData(); }, [period, energyType]);
const periodLabels: Record<string, [string, string]> = {
month: ['本月', '上月'],
week: ['本周', '上周'],
day: ['今日', '昨日'],
};
const [curLabel, prevLabel] = periodLabels[period] || ['当前', '上期'];
const chartOption = data ? {
tooltip: { trigger: 'axis' },
legend: { data: [curLabel, prevLabel] },
grid: { top: 50, right: 40, bottom: 30, left: 60 },
xAxis: { type: 'category', data: data.items.map(d => d.label) },
yAxis: { type: 'value', name: 'kWh' },
series: [
{
name: curLabel,
type: 'line',
smooth: true,
data: data.items.map(d => d.current_period),
lineStyle: { color: '#1890ff' },
itemStyle: { color: '#1890ff' },
},
{
name: prevLabel,
type: 'line',
smooth: true,
data: data.items.map(d => d.previous_period),
lineStyle: { color: '#faad14', type: 'dashed' },
itemStyle: { color: '#faad14' },
},
],
} : {};
const changePct = data?.total_change_pct || 0;
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<span>:</span>
<Select value={period} onChange={setPeriod} style={{ width: 120 }}
options={[
{ label: '按月', value: 'month' },
{ label: '按周', value: 'week' },
{ label: '按日', value: 'day' },
]}
/>
<span>:</span>
<Select value={energyType} onChange={setEnergyType} style={{ width: 120 }}
options={[
{ label: '电力', value: 'electricity' },
{ label: '热力', value: 'heat' },
{ label: '水', value: 'water' },
{ label: '燃气', value: 'gas' },
]}
/>
</Space>
</Card>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} sm={8}>
<Card>
<Statistic title={`${curLabel}用量`} value={data?.total_current || 0} suffix="kWh" precision={2} />
</Card>
</Col>
<Col xs={24} sm={8}>
<Card>
<Statistic title={`${prevLabel}用量`} value={data?.total_previous || 0} suffix="kWh" precision={2} />
</Card>
</Col>
<Col xs={24} sm={8}>
<Card>
<Statistic
title="环比变化"
value={Math.abs(changePct)}
suffix="%"
prefix={changePct >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: changePct >= 0 ? '#f5222d' : '#52c41a' }}
precision={1}
/>
</Card>
</Col>
</Row>
<Card title="环比趋势" size="small">
{data && <ReactECharts option={chartOption} style={{ height: 350 }} />}
</Card>
</div>
);
}

View File

@@ -0,0 +1,222 @@
import { useEffect, useState } from 'react';
import { Card, Row, Col, DatePicker, Checkbox, Table, message } from 'antd';
import ReactECharts from 'echarts-for-react';
import dayjs, { type Dayjs } from 'dayjs';
import api from '../../services/api';
const { RangePicker } = DatePicker;
interface Category {
id: number;
name: string;
code: string;
color: string;
children?: Category[];
}
interface ByCategory {
id: number;
name: string;
code: string;
color: string;
consumption: number;
percentage: number;
}
interface RankingItem {
name: string;
color: string;
consumption: number;
}
interface TrendItem {
date: string;
category: string;
color: string;
consumption: number;
}
export default function SubitemAnalysis() {
const [categories, setCategories] = useState<Category[]>([]);
const [flatCategories, setFlatCategories] = useState<Category[]>([]);
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs().subtract(30, 'day'), dayjs(),
]);
const [byCategory, setByCategory] = useState<ByCategory[]>([]);
const [ranking, setRanking] = useState<RankingItem[]>([]);
const [trend, setTrend] = useState<TrendItem[]>([]);
const [loading, setLoading] = useState(false);
const flatten = (cats: Category[]): Category[] => {
const result: Category[] = [];
const walk = (list: Category[]) => {
for (const c of list) {
result.push(c);
if (c.children) walk(c.children);
}
};
walk(cats);
return result;
};
useEffect(() => {
(async () => {
try {
const cats = await api.get('/energy/categories') as any as Category[];
setCategories(cats);
const flat = flatten(cats);
setFlatCategories(flat);
setSelectedCodes(flat.map(c => c.code));
} catch {
message.error('加载分项类别失败');
}
})();
}, []);
useEffect(() => {
if (selectedCodes.length > 0) loadData();
}, [selectedCodes, dateRange]);
const loadData = async () => {
setLoading(true);
const params = {
start_date: dateRange[0].format('YYYY-MM-DD'),
end_date: dateRange[1].format('YYYY-MM-DD'),
energy_type: 'electricity',
};
try {
const [byCat, rank, trendData] = await Promise.all([
api.get('/energy/by-category', { params }),
api.get('/energy/category-ranking', { params }),
api.get('/energy/category-trend', { params }),
]);
setByCategory((byCat as any[]).filter(c => selectedCodes.includes(c.code)));
setRanking((rank as any[]).filter(c => selectedCodes.includes(c.name) || true));
setTrend(trendData as any[]);
} catch {
message.error('加载分项数据失败');
} finally {
setLoading(false);
}
};
const pieOption = {
tooltip: { trigger: 'item', formatter: '{b}: {c} kWh ({d}%)' },
legend: { orient: 'vertical' as const, right: 10, top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{d}%' },
data: byCategory.map(c => ({
name: c.name, value: c.consumption,
itemStyle: c.color ? { color: c.color } : undefined,
})),
}],
};
const barOption = {
tooltip: { trigger: 'axis' },
grid: { top: 10, right: 30, bottom: 30, left: 100 },
xAxis: { type: 'value' as const, name: 'kWh' },
yAxis: {
type: 'category' as const,
data: [...ranking].reverse().map(r => r.name),
},
series: [{
type: 'bar',
data: [...ranking].reverse().map(r => ({
value: r.consumption,
itemStyle: r.color ? { color: r.color } : undefined,
})),
}],
};
// Group trend data by category for line chart
const trendCategories = [...new Set(trend.map(t => t.category))];
const trendDates = [...new Set(trend.map(t => t.date))].sort();
const colorMap: Record<string, string> = {};
trend.forEach(t => { if (t.color) colorMap[t.category] = t.color; });
const lineOption = {
tooltip: { trigger: 'axis' },
legend: { data: trendCategories },
grid: { top: 40, right: 20, bottom: 30, left: 60 },
xAxis: {
type: 'category' as const,
data: trendDates.map(d => dayjs(d).format('MM/DD')),
},
yAxis: { type: 'value' as const, name: 'kWh' },
series: trendCategories.map(cat => ({
name: cat,
type: 'line',
smooth: true,
data: trendDates.map(d => {
const item = trend.find(t => t.date === d && t.category === cat);
return item ? item.consumption : 0;
}),
lineStyle: colorMap[cat] ? { color: colorMap[cat] } : undefined,
itemStyle: colorMap[cat] ? { color: colorMap[cat] } : undefined,
})),
};
const tableColumns = [
{ title: '分项名称', dataIndex: 'name' },
{ title: '用量 (kWh)', dataIndex: 'consumption', render: (v: number) => v?.toFixed(2) },
{ title: '占比 (%)', dataIndex: 'percentage', render: (v: number) => v?.toFixed(1) },
];
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Row gutter={16} align="middle">
<Col>
<span style={{ marginRight: 8 }}>:</span>
<RangePicker
value={dateRange}
onChange={(dates) => dates && setDateRange(dates as [Dayjs, Dayjs])}
/>
</Col>
<Col flex="auto">
<span style={{ marginRight: 8 }}>:</span>
<Checkbox.Group
value={selectedCodes}
onChange={(vals) => setSelectedCodes(vals as string[])}
options={flatCategories.map(c => ({ label: c.name, value: c.code }))}
/>
</Col>
</Row>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Card title="能耗构成" size="small" loading={loading}>
<ReactECharts option={pieOption} style={{ height: 320 }} />
</Card>
</Col>
<Col xs={24} md={12}>
<Card title="分项排名" size="small" loading={loading}>
<ReactECharts option={barOption} style={{ height: 320 }} />
</Card>
</Col>
</Row>
<Card title="分项趋势" size="small" style={{ marginTop: 16 }} loading={loading}>
<ReactECharts option={lineOption} style={{ height: 350 }} />
</Card>
<Card title="分项明细" size="small" style={{ marginTop: 16 }}>
<Table
columns={tableColumns}
dataSource={byCategory}
rowKey="code"
size="small"
loading={loading}
pagination={false}
/>
</Card>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import { useEffect, useState } from 'react';
import { Card, Table, DatePicker, Select, Space, message } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import dayjs from 'dayjs';
import { getEnergyYoy } from '../../services/api';
interface YoyItem {
month: number;
current_year: number;
previous_year: number;
change_pct: number;
}
export default function YoyAnalysis() {
const [data, setData] = useState<YoyItem[]>([]);
const [loading, setLoading] = useState(false);
const [year, setYear] = useState(dayjs().year());
const [energyType, setEnergyType] = useState('electricity');
const loadData = async () => {
setLoading(true);
try {
const res = await getEnergyYoy({ year, energy_type: energyType });
setData(res as YoyItem[]);
} catch {
message.error('加载同比数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => { loadData(); }, [year, energyType]);
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
const chartOption = {
tooltip: { trigger: 'axis' },
legend: { data: [`${year}`, `${year - 1}`] },
grid: { top: 50, right: 40, bottom: 30, left: 60 },
xAxis: { type: 'category', data: months },
yAxis: { type: 'value', name: 'kWh' },
series: [
{
name: `${year}`,
type: 'bar',
data: data.map(d => d.current_year),
itemStyle: { color: '#1890ff' },
},
{
name: `${year - 1}`,
type: 'bar',
data: data.map(d => d.previous_year),
itemStyle: { color: '#faad14' },
},
],
};
const columns = [
{ title: '月份', dataIndex: 'month', render: (v: number) => `${v}` },
{ title: `${year}年 (kWh)`, dataIndex: 'current_year', render: (v: number) => v?.toFixed(2) },
{ title: `${year - 1}年 (kWh)`, dataIndex: 'previous_year', render: (v: number) => v?.toFixed(2) },
{
title: '同比变化',
dataIndex: 'change_pct',
render: (v: number) => (
<span style={{ color: v > 0 ? '#f5222d' : v < 0 ? '#52c41a' : '#666' }}>
{v > 0 ? <ArrowUpOutlined /> : v < 0 ? <ArrowDownOutlined /> : null}
{' '}{Math.abs(v).toFixed(1)}%
</span>
),
},
];
const yearOptions = [];
for (let y = dayjs().year(); y >= dayjs().year() - 5; y--) {
yearOptions.push({ label: `${y}`, value: y });
}
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<span>:</span>
<Select value={year} onChange={setYear} style={{ width: 120 }} options={yearOptions} />
<span>:</span>
<Select value={energyType} onChange={setEnergyType} style={{ width: 120 }}
options={[
{ label: '电力', value: 'electricity' },
{ label: '热力', value: 'heat' },
{ label: '水', value: 'water' },
{ label: '燃气', value: 'gas' },
]}
/>
</Space>
</Card>
<Card title="同比分析" size="small" style={{ marginBottom: 16 }}>
<ReactECharts option={chartOption} style={{ height: 350 }} />
</Card>
<Card title="月度明细" size="small">
<Table columns={columns} dataSource={data} rowKey="month"
loading={loading} size="small" pagination={false} />
</Card>
</div>
);
}

View File

@@ -4,6 +4,11 @@ import { ArrowUpOutlined, ArrowDownOutlined, DownloadOutlined, SwapOutlined } fr
import ReactECharts from 'echarts-for-react';
import dayjs, { type Dayjs } from 'dayjs';
import { getEnergyHistory, getEnergyComparison, getEnergyDailySummary, exportEnergyData } from '../../services/api';
import LossAnalysis from './LossAnalysis';
import YoyAnalysis from './YoyAnalysis';
import MomAnalysis from './MomAnalysis';
import CostAnalysis from './CostAnalysis';
import SubitemAnalysis from './SubitemAnalysis';
const { RangePicker } = DatePicker;
@@ -319,6 +324,11 @@ export default function Analysis() {
items={[
{ key: 'overview', label: '能耗概览', children: overviewContent },
{ key: 'comparison', label: '数据对比', children: <ComparisonView /> },
{ key: 'loss', label: '损耗分析', children: <LossAnalysis /> },
{ key: 'yoy', label: '同比分析', children: <YoyAnalysis /> },
{ key: 'mom', label: '环比分析', children: <MomAnalysis /> },
{ key: 'cost', label: '费用分析', children: <CostAnalysis /> },
{ key: 'subitem', label: '分项分析', children: <SubitemAnalysis /> },
]}
/>
</div>

View File

@@ -0,0 +1,169 @@
import { useEffect, useState } from 'react';
import { Card, Row, Col, Statistic, message } from 'antd';
import { DollarOutlined, ThunderboltOutlined, CarOutlined, DashboardOutlined } from '@ant-design/icons';
import { getChargingDashboard } from '../../services/api';
import ReactECharts from 'echarts-for-react';
export default function ChargingDashboard() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadDashboard();
}, []);
const loadDashboard = async () => {
setLoading(true);
try {
const res = await getChargingDashboard();
setData(res);
} catch {
message.error('加载充电总览失败');
} finally {
setLoading(false);
}
};
const pileStatusData = data ? [
{ type: '空闲', value: data.pile_status?.idle || 0 },
{ type: '充电中', value: data.pile_status?.charging || 0 },
{ type: '故障', value: data.pile_status?.fault || 0 },
{ type: '离线', value: data.pile_status?.offline || 0 },
].filter(d => d.value > 0) : [];
const pileStatusColors: Record<string, string> = {
'空闲': '#52c41a',
'充电中': '#1890ff',
'故障': '#ff4d4f',
'离线': '#d9d9d9',
};
const revenueLineOption = {
tooltip: { trigger: 'axis' as const },
xAxis: {
type: 'category' as const,
data: (data?.revenue_trend || []).map((d: any) => d.date),
axisLabel: { rotate: 45 },
},
yAxis: { type: 'value' as const, name: '营收 (元)' },
series: [{
type: 'line',
data: (data?.revenue_trend || []).map((d: any) => d.revenue),
smooth: true,
symbolSize: 6,
}],
grid: { left: 60, right: 20, bottom: 60, top: 30 },
};
const pieOption = {
tooltip: { trigger: 'item' as const },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: pileStatusData.map(d => ({
name: d.type,
value: d.value,
itemStyle: { color: pileStatusColors[d.type] || '#d9d9d9' },
})),
label: { formatter: '{b} {c}' },
}],
};
const barOption = {
tooltip: { trigger: 'axis' as const },
xAxis: { type: 'value' as const, name: '营收 (元)' },
yAxis: {
type: 'category' as const,
data: (data?.station_ranking || []).map((d: any) => d.station),
},
series: [{
type: 'bar',
data: (data?.station_ranking || []).map((d: any) => d.revenue),
label: { show: true, position: 'right' },
}],
grid: { left: 120, right: 40, bottom: 20, top: 20 },
};
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}>
<Card size="small" loading={loading}>
<Statistic
title="总营收 (元)"
value={data?.total_revenue || 0}
prefix={<DollarOutlined />}
precision={2}
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" loading={loading}>
<Statistic
title="充电量 (kWh)"
value={data?.total_energy || 0}
prefix={<ThunderboltOutlined />}
precision={1}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" loading={loading}>
<Statistic
title="充电中"
value={data?.active_sessions || 0}
prefix={<CarOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" loading={loading}>
<Statistic
title="利用率"
value={data?.utilization_rate || 0}
prefix={<DashboardOutlined />}
suffix="%"
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col xs={24} lg={16}>
<Card size="small" title="营收趋势 (近30天)" loading={loading}>
{data?.revenue_trend?.length > 0 ? (
<ReactECharts option={revenueLineOption} style={{ height: 300 }} />
) : (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}></div>
)}
</Card>
</Col>
<Col xs={24} lg={8}>
<Card size="small" title="充电桩状态分布" loading={loading}>
{pileStatusData.length > 0 ? (
<ReactECharts option={pieOption} style={{ height: 300 }} />
) : (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}></div>
)}
</Card>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Card size="small" title="充电站营收排名" loading={loading}>
{data?.station_ranking?.length > 0 ? (
<ReactECharts option={barOption} style={{ height: 300 }} />
) : (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}></div>
)}
</Card>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,201 @@
import { useEffect, useState, useCallback } from 'react';
import { Card, Table, Tag, Button, Tabs, Space, DatePicker, Select, message } from 'antd';
import { SyncOutlined, WarningOutlined } from '@ant-design/icons';
import { getChargingOrders, getChargingRealtimeOrders, getChargingAbnormalOrders, settleChargingOrder } from '../../services/api';
const { RangePicker } = DatePicker;
const orderStatusMap: Record<string, { color: string; text: string }> = {
charging: { color: 'processing', text: '充电中' },
pending_pay: { color: 'warning', text: '待支付' },
completed: { color: 'success', text: '已完成' },
failed: { color: 'error', text: '失败' },
refunded: { color: 'default', text: '已退款' },
};
const formatDuration = (seconds: number | null) => {
if (!seconds) return '-';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return h > 0 ? `${h}${m}` : `${m}`;
};
export default function Orders() {
return (
<Tabs
defaultActiveKey="realtime"
items={[
{ key: 'realtime', label: '实时充电', children: <RealtimeOrders /> },
{ key: 'history', label: '历史订单', children: <HistoryOrders /> },
{ key: 'abnormal', label: '异常订单', children: <AbnormalOrders /> },
]}
/>
);
}
function RealtimeOrders() {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const loadData = async () => {
setLoading(true);
try {
const res = await getChargingRealtimeOrders();
setData(res as any[]);
} catch { message.error('加载实时充电失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadData(); }, []);
const columns = [
{ title: '订单号', dataIndex: 'order_no', width: 160 },
{ title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true },
{ title: '充电桩', dataIndex: 'pile_name', width: 120 },
{ title: '用户', dataIndex: 'user_name', width: 100 },
{ title: '车牌', dataIndex: 'car_no', width: 100 },
{ title: '开始时间', dataIndex: 'start_time', width: 170 },
{ title: '时长', dataIndex: 'charge_duration', width: 80, render: formatDuration },
{ title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '起始SOC', dataIndex: 'start_soc', width: 90, render: (v: number) => v != null ? `${v}%` : '-' },
{ title: '当前SOC', dataIndex: 'end_soc', width: 90, render: (v: number) => v != null ? `${v}%` : '-' },
{ title: '状态', dataIndex: 'order_status', width: 90, render: () => (
<Tag icon={<SyncOutlined spin />} color="processing"></Tag>
)},
];
return (
<Card size="small" extra={<Button onClick={loadData}></Button>}>
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} size="small" scroll={{ x: 1300 }}
pagination={false} />
</Card>
);
}
function HistoryOrders() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
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 getChargingOrders(cleanQuery);
setData(res as any);
} catch { message.error('加载订单失败'); }
finally { setLoading(false); }
}, [filters]);
useEffect(() => { loadData(); }, [filters, loadData]);
const handleDateChange = (_: any, dates: [string, string]) => {
setFilters(prev => ({
...prev,
start_date: dates[0] || undefined,
end_date: dates[1] || undefined,
page: 1,
}));
};
const columns = [
{ title: '订单号', dataIndex: 'order_no', width: 160 },
{ title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true },
{ title: '充电桩', dataIndex: 'pile_name', width: 120 },
{ title: '用户', dataIndex: 'user_name', width: 100 },
{ title: '车牌', dataIndex: 'car_no', width: 100 },
{ title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '时长', dataIndex: 'charge_duration', width: 80, render: formatDuration },
{ title: '电费(元)', dataIndex: 'elec_amt', width: 90, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '服务费(元)', dataIndex: 'serve_amt', width: 100, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '实付(元)', dataIndex: 'paid_price', width: 90, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '状态', dataIndex: 'order_status', width: 90, render: (s: string) => {
const st = orderStatusMap[s] || { color: 'default', text: s || '-' };
return <Tag color={st.color}>{st.text}</Tag>;
}},
{ title: '创建时间', dataIndex: 'created_at', width: 170 },
];
return (
<Card size="small">
<Space wrap style={{ marginBottom: 16 }}>
<Select allowClear placeholder="订单状态" style={{ width: 120 }}
options={Object.entries(orderStatusMap).map(([k, v]) => ({ label: v.text, value: k }))}
onChange={v => setFilters(prev => ({ ...prev, order_status: v, page: 1 }))} />
<RangePicker onChange={handleDateChange} />
</Space>
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small" scroll={{ x: 1500 }}
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 })),
}}
/>
</Card>
);
}
function AbnormalOrders() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
const loadData = useCallback(async () => {
setLoading(true);
try {
const res = await getChargingAbnormalOrders(filters);
setData(res as any);
} catch { message.error('加载异常订单失败'); }
finally { setLoading(false); }
}, [filters]);
useEffect(() => { loadData(); }, [filters, loadData]);
const handleSettle = async (id: number) => {
try {
await settleChargingOrder(id);
message.success('手动结算成功');
loadData();
} catch { message.error('结算失败'); }
};
const columns = [
{ title: '订单号', dataIndex: 'order_no', width: 160 },
{ title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true },
{ title: '充电桩', dataIndex: 'pile_name', width: 120 },
{ title: '用户', dataIndex: 'user_name', width: 100 },
{ title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '状态', dataIndex: 'order_status', width: 90, render: (s: string) => {
const st = orderStatusMap[s] || { color: 'default', text: s || '-' };
return <Tag icon={<WarningOutlined />} color={st.color}>{st.text}</Tag>;
}},
{ title: '异常原因', dataIndex: 'abno_cause', width: 200, ellipsis: true },
{ title: '创建时间', dataIndex: 'created_at', width: 170 },
{ title: '操作', key: 'action', width: 100, render: (_: any, record: any) => (
record.order_status === 'failed' && (
<Button size="small" type="primary" onClick={() => handleSettle(record.id)}></Button>
)
)},
];
return (
<Card size="small">
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small" scroll={{ x: 1300 }}
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 })),
}}
/>
</Card>
);
}

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

View File

@@ -0,0 +1,193 @@
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, InputNumber, Space, message } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, MinusCircleOutlined } from '@ant-design/icons';
import { getChargingPricing, createChargingPricing, updateChargingPricing, deleteChargingPricing, getChargingStations } from '../../services/api';
const periodMarkMap: Record<string, { color: string; text: string }> = {
sharp: { color: 'red', text: '尖峰' },
peak: { color: 'orange', text: '高峰' },
flat: { color: 'blue', text: '平段' },
valley: { color: 'green', text: '低谷' },
};
export default function Pricing() {
const [data, setData] = useState<any[]>([]);
const [stations, setStations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [form] = Form.useForm();
const loadData = async () => {
setLoading(true);
try {
const res = await getChargingPricing();
setData(res as any[]);
} catch { message.error('加载计费策略失败'); }
finally { setLoading(false); }
};
const loadStations = async () => {
try {
const res = await getChargingStations({ page_size: 100 });
setStations((res as any).items || []);
} catch {}
};
useEffect(() => { loadData(); loadStations(); }, []);
const openAddModal = () => {
setEditing(null);
form.resetFields();
form.setFieldsValue({ status: 'inactive', bill_model: 'tou', params: [{}] });
setShowModal(true);
};
const openEditModal = (record: any) => {
setEditing(record);
form.setFieldsValue({
strategy_name: record.strategy_name,
station_id: record.station_id,
bill_model: record.bill_model,
description: record.description,
status: record.status,
params: record.params?.length > 0 ? record.params : [{}],
});
setShowModal(true);
};
const handleSubmit = async (values: any) => {
try {
if (editing) {
await updateChargingPricing(editing.id, values);
message.success('计费策略更新成功');
} else {
await createChargingPricing(values);
message.success('计费策略创建成功');
}
setShowModal(false);
form.resetFields();
loadData();
} catch (e: any) {
message.error(e?.detail || '操作失败');
}
};
const handleDelete = async (id: number) => {
try {
await deleteChargingPricing(id);
message.success('已停用');
loadData();
} catch { message.error('操作失败'); }
};
const columns = [
{ title: '策略名称', dataIndex: 'strategy_name', width: 180 },
{ 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: 'bill_model', width: 100, render: (v: string) => v === 'tou' ? '分时计费' : v === 'flat' ? '固定计费' : v || '-' },
{ title: '时段数', dataIndex: 'params', width: 80, render: (p: any[]) => p?.length || 0 },
{ title: '状态', dataIndex: 'status', width: 90, render: (s: string) => (
<Tag color={s === 'active' ? 'green' : 'default'}>{s === 'active' ? '启用' : '停用'}</Tag>
)},
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
{ title: '操作', key: 'action', width: 150, 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>
)},
];
const expandedRowRender = (record: any) => {
const paramCols = [
{ title: '开始时间', dataIndex: 'start_time', width: 100 },
{ title: '结束时间', dataIndex: 'end_time', width: 100 },
{ title: '时段标识', dataIndex: 'period_mark', width: 100, render: (v: string) => {
const pm = periodMarkMap[v];
return pm ? <Tag color={pm.color}>{pm.text}</Tag> : v || '-';
}},
{ title: '电费(元/kWh)', dataIndex: 'elec_price', width: 120, render: (v: number) => v?.toFixed(4) },
{ title: '服务费(元/kWh)', dataIndex: 'service_price', width: 130, render: (v: number) => v?.toFixed(4) },
{ title: '合计(元/kWh)', key: 'total', width: 120, render: (_: any, r: any) => ((r.elec_price || 0) + (r.service_price || 0)).toFixed(4) },
];
return <Table columns={paramCols} dataSource={record.params || []} rowKey="id" pagination={false} size="small" />;
};
return (
<Card size="small" title="计费策略管理" extra={
<Button type="primary" icon={<PlusOutlined />} onClick={openAddModal}></Button>
}>
<Table
columns={columns} dataSource={data} rowKey="id"
loading={loading} size="small"
expandable={{ expandedRowRender }}
pagination={false}
/>
<Modal
title={editing ? '编辑计费策略' : '新建计费策略'}
open={showModal}
onCancel={() => { setShowModal(false); form.resetFields(); }}
onOk={() => form.submit()}
okText={editing ? '保存' : '创建'}
cancelText="取消"
width={800}
destroyOnClose
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="strategy_name" label="策略名称" rules={[{ required: true, message: '请输入策略名称' }]}>
<Input placeholder="请输入策略名称" />
</Form.Item>
<Form.Item name="station_id" label="适用站点">
<Select allowClear placeholder="选择站点 (留空表示全部)"
options={stations.map((s: any) => ({ label: s.name, value: s.id }))} />
</Form.Item>
<Form.Item name="bill_model" label="计费模式">
<Select options={[{ label: '分时计费', value: 'tou' }, { label: '固定计费', value: 'flat' }]} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="策略描述" />
</Form.Item>
<Form.Item name="status" label="状态">
<Select options={[{ label: '启用', value: 'active' }, { label: '停用', value: 'inactive' }]} />
</Form.Item>
<div style={{ marginBottom: 8, fontWeight: 500 }}></div>
<Form.List name="params">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline" wrap>
<Form.Item {...restField} name={[name, 'start_time']} rules={[{ required: true, message: '开始时间' }]}>
<Input placeholder="开始 HH:MM" style={{ width: 110 }} />
</Form.Item>
<Form.Item {...restField} name={[name, 'end_time']} rules={[{ required: true, message: '结束时间' }]}>
<Input placeholder="结束 HH:MM" style={{ width: 110 }} />
</Form.Item>
<Form.Item {...restField} name={[name, 'period_mark']}>
<Select placeholder="时段" style={{ width: 100 }} allowClear
options={Object.entries(periodMarkMap).map(([k, v]) => ({ label: v.text, value: k }))} />
</Form.Item>
<Form.Item {...restField} name={[name, 'elec_price']} rules={[{ required: true, message: '电费' }]}>
<InputNumber placeholder="电费" style={{ width: 110 }} min={0} step={0.01} addonAfter="元" />
</Form.Item>
<Form.Item {...restField} name={[name, 'service_price']}>
<InputNumber placeholder="服务费" style={{ width: 120 }} min={0} step={0.01} addonAfter="元" />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} style={{ color: '#ff4d4f' }} />
</Space>
))}
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
</>
)}
</Form.List>
</Form>
</Modal>
</Card>
);
}

View File

@@ -0,0 +1,180 @@
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 { getChargingStations, createChargingStation, updateChargingStation, deleteChargingStation, getChargingMerchants } from '../../services/api';
const statusMap: Record<string, { color: string; text: string }> = {
active: { color: 'green', text: '运营中' },
disabled: { color: 'default', text: '已停用' },
};
const typeOptions = [
{ label: '公共充电站', value: 'public' },
{ label: '专用充电站', value: 'private' },
{ label: '专属充电站', value: 'dedicated' },
];
export default function Stations() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [merchants, setMerchants] = 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 getChargingStations(cleanQuery);
setData(res as any);
} catch { message.error('加载充电站失败'); }
finally { setLoading(false); }
}, [filters]);
const loadMerchants = async () => {
try {
const res = await getChargingMerchants();
setMerchants(res as any[]);
} catch {}
};
useEffect(() => { loadMerchants(); }, []);
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' });
setShowModal(true);
};
const openEditModal = (record: any) => {
setEditing(record);
form.setFieldsValue(record);
setShowModal(true);
};
const handleSubmit = async (values: any) => {
try {
if (editing) {
await updateChargingStation(editing.id, values);
message.success('充电站更新成功');
} else {
await createChargingStation(values);
message.success('充电站创建成功');
}
setShowModal(false);
form.resetFields();
loadData();
} catch (e: any) {
message.error(e?.detail || '操作失败');
}
};
const handleDelete = async (id: number) => {
try {
await deleteChargingStation(id);
message.success('已停用');
loadData();
} catch { message.error('操作失败'); }
};
const columns = [
{ title: '站点名称', dataIndex: 'name', width: 180, ellipsis: true },
{ title: '类型', dataIndex: 'type', width: 100, render: (v: string) => typeOptions.find(o => o.value === v)?.label || v || '-' },
{ title: '地址', dataIndex: 'address', width: 200, ellipsis: true },
{ title: '充电桩总数', dataIndex: 'total_piles', width: 100 },
{ title: '可用桩数', dataIndex: 'available_piles', width: 100 },
{ title: '总功率(kW)', dataIndex: 'total_power_kw', width: 110 },
{ title: '默认电价(元/kWh)', dataIndex: 'price', width: 140, render: (v: number) => v != null ? v.toFixed(2) : '-' },
{ title: '运营时间', dataIndex: 'operating_hours', width: 120 },
{ title: '状态', dataIndex: 'status', width: 90, render: (s: string) => {
const st = statusMap[s] || { color: 'default', text: s || '-' };
return <Tag color={st.color}>{st.text}</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: 150 }} options={typeOptions}
onChange={v => handleFilterChange('type', v)} />
<Select allowClear placeholder="状态" style={{ width: 120 }}
options={[{ label: '运营中', value: 'active' }, { label: '已停用', value: 'disabled' }]}
onChange={v => handleFilterChange('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="name" label="站点名称" rules={[{ required: true, message: '请输入站点名称' }]}>
<Input placeholder="请输入站点名称" />
</Form.Item>
<Form.Item name="type" label="类型">
<Select allowClear placeholder="选择类型" options={typeOptions} />
</Form.Item>
<Form.Item name="merchant_id" label="运营商">
<Select allowClear placeholder="选择运营商"
options={merchants.map((m: any) => ({ label: m.name, value: m.id }))} />
</Form.Item>
<Form.Item name="address" label="地址">
<Input placeholder="请输入地址" />
</Form.Item>
<Form.Item name="price" label="默认电价(元/kWh)">
<InputNumber style={{ width: '100%' }} min={0} step={0.01} placeholder="默认电价" />
</Form.Item>
<Form.Item name="total_power_kw" label="总功率(kW)">
<InputNumber style={{ width: '100%' }} min={0} step={1} />
</Form.Item>
<Form.Item name="operating_hours" label="运营时间">
<Input placeholder="例: 00:00-24:00" />
</Form.Item>
<Form.Item name="activity" label="优惠活动">
<Input.TextArea rows={2} placeholder="优惠活动信息" />
</Form.Item>
<Form.Item name="status" label="状态">
<Select options={[{ label: '运营中', value: 'active' }, { label: '已停用', value: 'disabled' }]} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}

View File

@@ -0,0 +1,23 @@
import { Tabs } from 'antd';
import ChargingDashboard from './Dashboard';
import Stations from './Stations';
import Piles from './Piles';
import Orders from './Orders';
import Pricing from './Pricing';
export default function Charging() {
return (
<div>
<Tabs
defaultActiveKey="dashboard"
items={[
{ key: 'dashboard', label: '充电总览', children: <ChargingDashboard /> },
{ key: 'stations', label: '充电站', children: <Stations /> },
{ key: 'piles', label: '充电桩', children: <Piles /> },
{ key: 'orders', label: '充电订单', children: <Orders /> },
{ key: 'pricing', label: '计费策略', children: <Pricing /> },
]}
/>
</div>
);
}

View File

@@ -0,0 +1,365 @@
import { useEffect, useState, useCallback } from 'react';
import { Card, Row, Col, Tree, Checkbox, DatePicker, Select, Button, Table, Space, message } from 'antd';
import { SearchOutlined, DownloadOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import dayjs, { type Dayjs } from 'dayjs';
import { getDeviceGroups, getDevices, queryElectricalParams, exportEnergyData } from '../../services/api';
import type { DataNode } from 'antd/es/tree';
const { RangePicker } = DatePicker;
const PARAM_OPTIONS = [
{ label: '功率 (kW)', value: 'power' },
{ label: '电压 (V)', value: 'voltage' },
{ label: '电流 (A)', value: 'current' },
{ label: '功率因数', value: 'power_factor' },
{ label: '温度 (℃)', value: 'temperature' },
{ label: '频率 (Hz)', value: 'frequency' },
{ label: 'COP', value: 'cop' },
];
const PARAM_UNITS: Record<string, string> = {
power: 'kW',
voltage: 'V',
current: 'A',
power_factor: '',
temperature: '℃',
frequency: 'Hz',
cop: '',
};
const PARAM_COLORS = ['#1890ff', '#f5222d', '#52c41a', '#faad14', '#722ed1', '#13c2c2', '#eb2f96'];
const GRANULARITY_OPTIONS = [
{ label: '原始', value: 'raw' },
{ label: '5分钟', value: '5min' },
{ label: '按小时', value: 'hour' },
{ label: '按天', value: 'day' },
];
export default function DataQuery() {
const [treeData, setTreeData] = useState<DataNode[]>([]);
const [deviceMap, setDeviceMap] = useState<Record<number, any>>({});
const [selectedDeviceId, setSelectedDeviceId] = useState<number | null>(null);
const [selectedParams, setSelectedParams] = useState<string[]>(['power']);
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs().subtract(7, 'day'),
dayjs(),
]);
const [granularity, setGranularity] = useState('hour');
const [chartData, setChartData] = useState<Record<string, any[]>>({});
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
useEffect(() => {
loadTree();
}, []);
const loadTree = async () => {
try {
const [groups, devicesRes] = await Promise.all([
getDeviceGroups(),
getDevices({ page_size: 100 }),
]);
const groupList = groups as any[];
const devices = (devicesRes as any).items || [];
const dMap: Record<number, any> = {};
devices.forEach((d: any) => { dMap[d.id] = d; });
setDeviceMap(dMap);
const groupMap: Record<number, any> = {};
groupList.forEach(g => {
groupMap[g.id] = { ...g, children: [], devices: [] };
});
devices.forEach((d: any) => {
if (d.group_id && groupMap[d.group_id]) {
groupMap[d.group_id].devices.push(d);
}
});
const buildTree = (parentId: number | null): DataNode[] => {
const nodes: DataNode[] = [];
Object.values(groupMap).forEach((g: any) => {
if (g.parent_id === parentId) {
const childNodes = buildTree(g.id);
const deviceNodes: DataNode[] = g.devices.map((d: any) => ({
title: d.name,
key: `device-${d.id}`,
isLeaf: true,
}));
nodes.push({
title: `${g.name}${g.location ? ` (${g.location})` : ''}`,
key: `group-${g.id}`,
children: [...childNodes, ...deviceNodes],
selectable: false,
});
}
});
return nodes;
};
const tree = buildTree(null);
// Add ungrouped devices
const ungrouped = devices.filter((d: any) => !d.group_id);
if (ungrouped.length > 0) {
tree.push({
title: '未分组设备',
key: 'group-ungrouped',
children: ungrouped.map((d: any) => ({
title: d.name,
key: `device-${d.id}`,
isLeaf: true,
})),
selectable: false,
});
}
setTreeData(tree);
} catch (e) {
console.error(e);
}
};
const handleTreeSelect = (selectedKeys: any[]) => {
if (selectedKeys.length > 0) {
const key = selectedKeys[0] as string;
if (key.startsWith('device-')) {
setSelectedDeviceId(parseInt(key.replace('device-', '')));
}
}
};
const handleQuery = useCallback(async () => {
if (!selectedDeviceId) {
message.warning('请先选择设备');
return;
}
if (selectedParams.length === 0) {
message.warning('请至少选择一个参数');
return;
}
setLoading(true);
try {
const res = await queryElectricalParams({
device_id: selectedDeviceId,
params: selectedParams.join(','),
start_time: dateRange[0].format('YYYY-MM-DD HH:mm:ss'),
end_time: dateRange[1].format('YYYY-MM-DD HH:mm:ss'),
granularity,
});
setChartData(res as Record<string, any[]>);
} catch (e) {
console.error(e);
message.error('查询失败');
} finally {
setLoading(false);
}
}, [selectedDeviceId, selectedParams, dateRange, granularity]);
const handleExport = async (format: 'csv' | 'xlsx') => {
if (!selectedDeviceId) return;
setExporting(true);
try {
await exportEnergyData({
start_time: dateRange[0].format('YYYY-MM-DD'),
end_time: dateRange[1].format('YYYY-MM-DD'),
device_id: selectedDeviceId,
format,
});
message.success('导出成功');
} catch (e) {
message.error('导出失败');
} finally {
setExporting(false);
}
};
// Build chart option
const getChartOption = () => {
const paramKeys = Object.keys(chartData);
if (paramKeys.length === 0) return {};
// Collect all timestamps from all params
const allTimestamps = new Set<string>();
paramKeys.forEach(p => {
(chartData[p] || []).forEach((d: any) => allTimestamps.add(d.timestamp));
});
const timestamps = Array.from(allTimestamps).sort();
// Build unique units for y-axes
const units: string[] = [];
paramKeys.forEach(p => {
const unit = PARAM_UNITS[p] || '';
if (!units.includes(unit)) units.push(unit);
});
const yAxes = units.map((unit, i) => ({
type: 'value' as const,
name: unit,
position: i === 0 ? 'left' as const : 'right' as const,
offset: i > 1 ? (i - 1) * 60 : 0,
axisLine: { show: true },
}));
const series = paramKeys.map((param, i) => {
const dataMap = new Map<string, number>();
(chartData[param] || []).forEach((d: any) => {
dataMap.set(d.timestamp, d.value);
});
const unit = PARAM_UNITS[param] || '';
const yAxisIndex = units.indexOf(unit);
const label = PARAM_OPTIONS.find(o => o.value === param)?.label || param;
return {
name: label,
type: 'line' as const,
smooth: true,
yAxisIndex,
data: timestamps.map(t => dataMap.get(t) ?? null),
lineStyle: { color: PARAM_COLORS[i % PARAM_COLORS.length] },
itemStyle: { color: PARAM_COLORS[i % PARAM_COLORS.length] },
connectNulls: true,
};
});
return {
tooltip: { trigger: 'axis' },
legend: { data: series.map(s => s.name) },
grid: { top: 50, right: units.length > 1 ? 80 + (units.length - 2) * 60 : 40, bottom: 30, left: 60 },
xAxis: {
type: 'category',
data: timestamps.map(t => {
const d = new Date(t);
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}),
axisLabel: { rotate: 30 },
},
yAxis: yAxes.length > 0 ? yAxes : [{ type: 'value' }],
series,
dataZoom: [{ type: 'inside' }, { type: 'slider' }],
};
};
// Build table data
const getTableData = () => {
const paramKeys = Object.keys(chartData);
if (paramKeys.length === 0) return { columns: [], data: [] };
const allTimestamps = new Set<string>();
paramKeys.forEach(p => {
(chartData[p] || []).forEach((d: any) => allTimestamps.add(d.timestamp));
});
const timestamps = Array.from(allTimestamps).sort();
const dataMaps: Record<string, Map<string, number>> = {};
paramKeys.forEach(p => {
dataMaps[p] = new Map();
(chartData[p] || []).forEach((d: any) => {
dataMaps[p].set(d.timestamp, d.value);
});
});
const columns = [
{ title: '时间', dataIndex: 'timestamp', width: 180 },
...paramKeys.map(p => ({
title: PARAM_OPTIONS.find(o => o.value === p)?.label || p,
dataIndex: p,
width: 120,
render: (v: number) => v != null ? v.toFixed(2) : '-',
})),
];
const data = timestamps.map((t, i) => {
const row: Record<string, any> = { key: i, timestamp: t };
paramKeys.forEach(p => {
row[p] = dataMaps[p].get(t) ?? null;
});
return row;
});
return { columns, data };
};
const tableInfo = getTableData();
const selectedDevice = selectedDeviceId ? deviceMap[selectedDeviceId] : null;
return (
<Row gutter={16} style={{ height: '100%' }}>
{/* Left Panel - Device Tree */}
<Col xs={24} md={6}>
<Card title="设备选择" size="small" style={{ height: '100%' }} bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflow: 'auto' }}>
<Tree
treeData={treeData}
onSelect={handleTreeSelect}
selectedKeys={selectedDeviceId ? [`device-${selectedDeviceId}`] : []}
defaultExpandAll
showLine
/>
</Card>
</Col>
{/* Right Panel - Query & Results */}
<Col xs={24} md={18}>
<Card size="small" style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 12 }}>
<span style={{ marginRight: 8, fontWeight: 500 }}>:</span>
<span style={{ color: selectedDevice ? '#1890ff' : '#999' }}>
{selectedDevice ? `${selectedDevice.name} (${selectedDevice.code})` : '请在左侧选择设备'}
</span>
</div>
<div style={{ marginBottom: 12 }}>
<span style={{ marginRight: 8, fontWeight: 500 }}>:</span>
<Checkbox.Group
options={PARAM_OPTIONS}
value={selectedParams}
onChange={(vals) => setSelectedParams(vals as string[])}
/>
</div>
<Space wrap>
<RangePicker
showTime
value={dateRange}
onChange={(dates) => dates && setDateRange(dates as [Dayjs, Dayjs])}
/>
<Select
value={granularity}
onChange={setGranularity}
style={{ width: 100 }}
options={GRANULARITY_OPTIONS}
/>
<Button type="primary" icon={<SearchOutlined />} loading={loading} onClick={handleQuery}>
</Button>
<Button icon={<DownloadOutlined />} loading={exporting} onClick={() => handleExport('csv')}>
CSV
</Button>
<Button icon={<DownloadOutlined />} loading={exporting} onClick={() => handleExport('xlsx')}>
Excel
</Button>
</Space>
</Card>
{Object.keys(chartData).length > 0 && (
<>
<Card title="数据趋势" size="small" style={{ marginBottom: 16 }}>
<ReactECharts option={getChartOption()} style={{ height: 400 }} />
</Card>
<Card title="数据列表" size="small">
<Table
columns={tableInfo.columns}
dataSource={tableInfo.data}
size="small"
scroll={{ x: 'max-content' }}
pagination={{ pageSize: 20, showTotal: (total: number) => `${total}` }}
/>
</Card>
</>
)}
</Col>
</Row>
);
}

View File

@@ -0,0 +1,186 @@
import { useEffect, useState } from 'react';
import { Card, Tree, Table, Tag, Badge, Button, Space, Row, Col, Empty } from 'antd';
import { ApartmentOutlined, ExpandOutlined, CompressOutlined } from '@ant-design/icons';
import { getDeviceTopology, getDevices } from '../../services/api';
import type { DataNode } from 'antd/es/tree';
const statusMap: Record<string, { color: string; text: string }> = {
online: { color: 'green', text: '在线' },
offline: { color: 'default', text: '离线' },
alarm: { color: 'red', text: '告警' },
maintenance: { color: 'orange', text: '维护' },
};
function getStatusDot(node: any): string {
if (node.total_alarm > 0) return '#f5222d';
if (node.total_offline > 0 && node.total_online > 0) return '#faad14';
if (node.total_online > 0) return '#52c41a';
if (node.total_device_count === 0) return '#d9d9d9';
return '#999';
}
function buildTreeNodes(nodes: any[]): DataNode[] {
return nodes.map((node: any) => {
const dotColor = getStatusDot(node);
const title = (
<span>
<span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
backgroundColor: dotColor, marginRight: 8,
}} />
{node.name}
{node.location ? <span style={{ color: '#999', marginLeft: 4 }}>({node.location})</span> : null}
<Badge
count={node.total_device_count || 0}
style={{ backgroundColor: '#1890ff', marginLeft: 8 }}
overflowCount={999}
size="small"
/>
</span>
);
return {
title,
key: `group-${node.id}`,
children: buildTreeNodes(node.children || []),
isLeaf: !node.children || node.children.length === 0,
};
});
}
export default function Topology() {
const [treeData, setTreeData] = useState<DataNode[]>([]);
const [topologyData, setTopologyData] = useState<any[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
const [selectedGroupName, setSelectedGroupName] = useState('');
const [devices, setDevices] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
useEffect(() => {
loadTopology();
}, []);
const loadTopology = async () => {
try {
const data = await getDeviceTopology() as any[];
setTopologyData(data);
setTreeData(buildTreeNodes(data));
// Expand all by default
const allKeys = collectKeys(data);
setExpandedKeys(allKeys);
} catch (e) {
console.error(e);
}
};
const collectKeys = (nodes: any[]): string[] => {
const keys: string[] = [];
nodes.forEach(n => {
keys.push(`group-${n.id}`);
if (n.children) {
keys.push(...collectKeys(n.children));
}
});
return keys;
};
const findGroupName = (nodes: any[], id: number): string => {
for (const n of nodes) {
if (n.id === id) return n.name;
if (n.children) {
const found = findGroupName(n.children, id);
if (found) return found;
}
}
return '';
};
const handleSelect = async (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) return;
const key = selectedKeys[0] as string;
const groupId = parseInt(key.replace('group-', ''));
setSelectedGroupId(groupId);
setSelectedGroupName(findGroupName(topologyData, groupId));
setLoading(true);
try {
const res = await getDevices({ group_id: groupId, page_size: 100 }) as any;
setDevices(res.items || []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const handleExpandAll = () => {
setExpandedKeys(collectKeys(topologyData));
};
const handleCollapseAll = () => {
setExpandedKeys([]);
};
const columns = [
{ title: '设备名称', dataIndex: 'name', width: 160 },
{ title: '设备编号', dataIndex: 'code', width: 130 },
{ title: '类型', dataIndex: 'device_type', width: 100 },
{
title: '状态', dataIndex: 'status', width: 80,
render: (s: string) => {
const st = statusMap[s] || { color: 'default', text: s || '-' };
return <Tag color={st.color}>{st.text}</Tag>;
},
},
{ title: '位置', dataIndex: 'location', width: 120, ellipsis: true },
{ title: '额定功率(kW)', dataIndex: 'rated_power', width: 110, render: (v: number) => v != null ? v : '-' },
{ title: '最近数据时间', dataIndex: 'last_data_time', width: 170 },
];
return (
<Row gutter={16}>
<Col xs={24} md={8}>
<Card
title={<><ApartmentOutlined /> </>}
size="small"
extra={
<Space>
<Button size="small" icon={<ExpandOutlined />} onClick={handleExpandAll}></Button>
<Button size="small" icon={<CompressOutlined />} onClick={handleCollapseAll}></Button>
</Space>
}
bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflow: 'auto' }}
>
<Tree
treeData={treeData}
expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys)}
onSelect={handleSelect}
showLine
blockNode
/>
</Card>
</Col>
<Col xs={24} md={16}>
<Card
title={selectedGroupName ? `${selectedGroupName} - 设备列表` : '设备列表'}
size="small"
>
{selectedGroupId ? (
<Table
columns={columns}
dataSource={devices}
rowKey="id"
size="small"
loading={loading}
scroll={{ x: 'max-content' }}
pagination={{ pageSize: 20, showTotal: (total: number) => `${total}` }}
/>
) : (
<Empty description="请在左侧拓扑树中选择一个分组" />
)}
</Card>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,399 @@
import { useEffect, useState } from 'react';
import {
Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber,
Space, message, Row, Col, Statistic, DatePicker, Badge,
} from 'antd';
import {
PlusOutlined, ToolOutlined, CheckOutlined, WarningOutlined,
ClockCircleOutlined, UserOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import {
getMaintenanceDashboard, getMaintenancePlans, createMaintenancePlan,
triggerMaintenancePlan, getMaintenanceRecords, getMaintenanceOrders,
createMaintenanceOrder, assignMaintenanceOrder, completeMaintenanceOrder,
getMaintenanceDuty, createMaintenanceDuty,
} from '../../services/api';
const priorityMap: Record<string, { color: string; text: string }> = {
critical: { color: 'red', text: '紧急' },
high: { color: 'orange', text: '高' },
medium: { color: 'blue', text: '中' },
low: { color: 'default', text: '低' },
};
const orderStatusMap: Record<string, { color: string; text: string }> = {
open: { color: 'red', text: '待处理' },
assigned: { color: 'orange', text: '已指派' },
in_progress: { color: 'processing', text: '处理中' },
completed: { color: 'green', text: '已完成' },
verified: { color: 'cyan', text: '已验证' },
closed: { color: 'default', text: '已关闭' },
};
const recordStatusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'default', text: '待执行' },
in_progress: { color: 'processing', text: '执行中' },
completed: { color: 'green', text: '已完成' },
issues_found: { color: 'orange', text: '发现问题' },
};
const shiftMap: Record<string, string> = {
day: '白班', night: '夜班', on_call: '值班',
};
// ── Tab 1: Dashboard ───────────────────────────────────────────────
function DashboardTab() {
const [dashboard, setDashboard] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
setDashboard(await getMaintenanceDashboard());
} catch { message.error('加载运维概览失败'); }
finally { setLoading(false); }
})();
}, []);
const orderColumns = [
{ title: '工单号', dataIndex: 'code', width: 160 },
{ title: '标题', dataIndex: 'title' },
{ title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => {
const p = priorityMap[v] || { color: 'default', text: v };
return <Tag color={p.color}>{p.text}</Tag>;
}},
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => {
const s = orderStatusMap[v] || { color: 'default', text: v };
return <Badge status={s.color as any} text={s.text} />;
}},
{ title: '创建时间', dataIndex: 'created_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}>
<Card><Statistic title="待处理工单" value={dashboard?.open_orders || 0} prefix={<ToolOutlined />} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card><Statistic title="逾期工单" value={dashboard?.overdue_count || 0} prefix={<WarningOutlined />} valueStyle={{ color: dashboard?.overdue_count > 0 ? '#f5222d' : undefined }} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card><Statistic title="今日巡检" value={dashboard?.todays_inspections || 0} prefix={<CheckOutlined />} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card><Statistic title="近期值班" value={dashboard?.upcoming_duties || 0} prefix={<ClockCircleOutlined />} /></Card>
</Col>
</Row>
<Card title="最近工单" size="small">
<Table columns={orderColumns} dataSource={dashboard?.recent_orders || []} rowKey="id" loading={loading} size="small" pagination={false} />
</Card>
</div>
);
}
// ── Tab 2: Inspection Plans ────────────────────────────────────────
function PlansTab() {
const [plans, setPlans] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [form] = Form.useForm();
const loadPlans = async () => {
setLoading(true);
try { setPlans(await getMaintenancePlans() as any[]); }
catch { message.error('加载巡检计划失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadPlans(); }, []);
const handleCreate = async (values: any) => {
try {
await createMaintenancePlan(values);
message.success('巡检计划创建成功');
setShowModal(false);
form.resetFields();
loadPlans();
} catch { message.error('创建失败'); }
};
const handleTrigger = async (id: number) => {
try {
await triggerMaintenancePlan(id);
message.success('已触发巡检');
} catch { message.error('触发失败'); }
};
const columns = [
{ title: '计划名称', dataIndex: 'name' },
{ title: '巡检周期', dataIndex: 'schedule_type', render: (v: string) => {
const map: Record<string, string> = { daily: '每日', weekly: '每周', monthly: '每月', custom: '自定义' };
return map[v] || v || '-';
}},
{ title: '状态', dataIndex: 'is_active', width: 80, render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag> },
{ title: '下次执行', dataIndex: 'next_run_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{ title: '操作', key: 'action', width: 120, render: (_: any, r: any) => (
<Button size="small" type="primary" onClick={() => handleTrigger(r.id)}></Button>
)},
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Table columns={columns} dataSource={plans} rowKey="id" loading={loading} size="small" />
<Modal title="新建巡检计划" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消">
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="name" label="计划名称" rules={[{ required: true }]}>
<Input placeholder="例: 每日热泵巡检" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} />
</Form.Item>
<Form.Item name="schedule_type" label="巡检周期" initialValue="daily">
<Select options={[
{ label: '每日', value: 'daily' }, { label: '每周', value: 'weekly' },
{ label: '每月', value: 'monthly' }, { label: '自定义', value: 'custom' },
]} />
</Form.Item>
<Form.Item name="schedule_cron" label="Cron表达式 (自定义周期)">
<Input placeholder="例: 0 8 * * 1-5" />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Tab 3: Inspection Records ──────────────────────────────────────
function RecordsTab() {
const [records, setRecords] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string | undefined>();
const loadRecords = async () => {
setLoading(true);
try {
setRecords(await getMaintenanceRecords({ status: statusFilter }));
} catch { message.error('加载巡检记录失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadRecords(); }, [statusFilter]);
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '计划ID', dataIndex: 'plan_id', width: 80 },
{ title: '巡检人', dataIndex: 'inspector_id', width: 80 },
{ title: '状态', dataIndex: 'status', width: 100, render: (v: string) => {
const s = recordStatusMap[v] || { color: 'default', text: v };
return <Tag color={s.color}>{s.text}</Tag>;
}},
{ title: '开始时间', dataIndex: 'started_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{ title: '完成时间', dataIndex: 'completed_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
return (
<Card size="small" extra={
<Select allowClear placeholder="状态筛选" style={{ width: 140 }} value={statusFilter} onChange={setStatusFilter}
options={Object.entries(recordStatusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
}>
<Table columns={columns} dataSource={records.items} rowKey="id" loading={loading} size="small"
pagination={{ total: records.total, pageSize: 20 }} />
</Card>
);
}
// ── Tab 4: Repair Orders ───────────────────────────────────────────
function OrdersTab() {
const [orders, setOrders] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [assignModal, setAssignModal] = useState<{ open: boolean; orderId: number | null }>({ open: false, orderId: null });
const [form] = Form.useForm();
const loadOrders = async () => {
setLoading(true);
try { setOrders(await getMaintenanceOrders({})); }
catch { message.error('加载工单失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadOrders(); }, []);
const handleCreate = async (values: any) => {
try {
await createMaintenanceOrder(values);
message.success('工单创建成功');
setShowModal(false);
form.resetFields();
loadOrders();
} catch { message.error('创建失败'); }
};
const handleAssign = async (userId: number) => {
if (!assignModal.orderId) return;
try {
await assignMaintenanceOrder(assignModal.orderId, userId);
message.success('已指派');
setAssignModal({ open: false, orderId: null });
loadOrders();
} catch { message.error('指派失败'); }
};
const handleComplete = async (id: number) => {
try {
await completeMaintenanceOrder(id);
message.success('已完成');
loadOrders();
} catch { message.error('操作失败'); }
};
const columns = [
{ title: '工单号', dataIndex: 'code', width: 160 },
{ title: '标题', dataIndex: 'title' },
{ title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => {
const p = priorityMap[v] || { color: 'default', text: v };
return <Tag color={p.color}>{p.text}</Tag>;
}},
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => {
const s = orderStatusMap[v] || { color: 'default', text: v };
return <Badge status={s.color as any} text={s.text} />;
}},
{ title: '创建时间', dataIndex: 'created_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{ title: '操作', key: 'action', width: 200, render: (_: any, r: any) => (
<Space>
{r.status === 'open' && <Button size="small" icon={<UserOutlined />} onClick={() => setAssignModal({ open: true, orderId: r.id })}></Button>}
{['assigned', 'in_progress'].includes(r.status) && <Button size="small" type="primary" icon={<CheckOutlined />} onClick={() => handleComplete(r.id)}></Button>}
</Space>
)},
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Table columns={columns} dataSource={orders.items} rowKey="id" loading={loading} size="small"
pagination={{ total: orders.total, pageSize: 20 }} />
<Modal title="新建维修工单" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消">
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="title" label="工单标题" rules={[{ required: true }]}>
<Input placeholder="例: 2#热泵压缩机异响" />
</Form.Item>
<Form.Item name="description" label="故障描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="device_id" label="关联设备ID">
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="priority" label="优先级" initialValue="medium">
<Select options={Object.entries(priorityMap).map(([k, v]) => ({ label: v.text, value: k }))} />
</Form.Item>
<Form.Item name="cost_estimate" label="预估费用">
<InputNumber style={{ width: '100%' }} prefix="¥" min={0} />
</Form.Item>
</Form>
</Modal>
<Modal title="指派工单" open={assignModal.open} onCancel={() => setAssignModal({ open: false, orderId: null })} footer={null}>
<Form layout="vertical" onFinish={(v) => handleAssign(v.assigned_to)}>
<Form.Item name="assigned_to" label="指派给 (用户ID)" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit"></Button>
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Tab 5: Duty Schedule ───────────────────────────────────────────
function DutyTab() {
const [duties, setDuties] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [form] = Form.useForm();
const loadDuties = async () => {
setLoading(true);
try {
const start = dayjs().subtract(7, 'day').format('YYYY-MM-DD');
const end = dayjs().add(30, 'day').format('YYYY-MM-DD');
setDuties(await getMaintenanceDuty({ start_date: start, end_date: end }) as any[]);
} catch { message.error('加载值班安排失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadDuties(); }, []);
const handleCreate = async (values: any) => {
try {
await createMaintenanceDuty({
...values,
duty_date: values.duty_date.format('YYYY-MM-DD'),
});
message.success('值班安排创建成功');
setShowModal(false);
form.resetFields();
loadDuties();
} catch { message.error('创建失败'); }
};
const columns = [
{ title: '日期', dataIndex: 'duty_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '班次', dataIndex: 'shift', width: 80, render: (v: string) => shiftMap[v] || v || '-' },
{ title: '人员ID', dataIndex: 'user_id', width: 80 },
{ title: '区域ID', dataIndex: 'area_id', width: 80, render: (v: number) => v || '-' },
{ title: '备注', dataIndex: 'notes' },
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Table columns={columns} dataSource={duties} rowKey="id" loading={loading} size="small" />
<Modal title="新建值班安排" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消">
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="user_id" label="人员ID" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="duty_date" label="值班日期" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="shift" label="班次" initialValue="day">
<Select options={[
{ label: '白班', value: 'day' }, { label: '夜班', value: 'night' }, { label: '值班', value: 'on_call' },
]} />
</Form.Item>
<Form.Item name="area_id" label="区域ID">
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Main Component ─────────────────────────────────────────────────
export default function Maintenance() {
return (
<div>
<Tabs items={[
{ key: 'dashboard', label: '概览', children: <DashboardTab /> },
{ key: 'plans', label: '巡检计划', children: <PlansTab /> },
{ key: 'records', label: '巡检记录', children: <RecordsTab /> },
{ key: 'orders', label: '维修工单', children: <OrdersTab /> },
{ key: 'duty', label: '值班安排', children: <DutyTab /> },
]} />
</div>
);
}

View File

@@ -0,0 +1,524 @@
import { useEffect, useState } from 'react';
import { Tabs, Card, Table, Tag, Button, Modal, Form, Input, Select, DatePicker, Space, message, Popconfirm, List, Badge } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import {
getRegulations, createRegulation, updateRegulation, deleteRegulation,
getStandards, createStandard, updateStandard, deleteStandard,
getProcessDocs, createProcessDoc, updateProcessDoc, deleteProcessDoc,
getEmergencyPlans, createEmergencyPlan, updateEmergencyPlan, deleteEmergencyPlan,
} from '../../services/api';
const { TextArea } = Input;
const REGULATION_CATEGORIES = [
{ label: '安全管理', value: 'safety' },
{ label: '运行管理', value: 'operation' },
{ label: '质量管理', value: 'quality' },
{ label: '环境管理', value: 'environment' },
];
const REGULATION_STATUS = [
{ label: '生效', value: 'active', color: 'green' },
{ label: '草稿', value: 'draft', color: 'default' },
{ label: '归档', value: 'archived', color: 'orange' },
];
const STANDARD_TYPES = [
{ label: '国家标准', value: 'national' },
{ label: '行业标准', value: 'industry' },
{ label: '企业标准', value: 'enterprise' },
];
const COMPLIANCE_STATUS = [
{ label: '已合规', value: 'compliant', color: 'green' },
{ label: '不合规', value: 'non_compliant', color: 'red' },
{ label: '待审核', value: 'pending', color: 'default' },
{ label: '整改中', value: 'in_progress', color: 'blue' },
];
const PROCESS_CATEGORIES = [
{ label: '操作规程', value: 'operation' },
{ label: '维护流程', value: 'maintenance' },
{ label: '应急流程', value: 'emergency' },
{ label: '培训流程', value: 'training' },
];
const SCENARIO_TYPES = [
{ label: '火灾', value: 'fire' },
{ label: '停电', value: 'power_outage' },
{ label: '设备故障', value: 'equipment_failure' },
{ label: '化学泄漏', value: 'chemical_leak' },
];
// ── Regulations Tab ──────────────────────────────────────────────────
function RegulationsTab() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
const [form] = Form.useForm();
const load = async () => {
setLoading(true);
try {
const res = await getRegulations(filters);
setData(res as any);
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, [filters]);
const handleSubmit = async (values: any) => {
try {
if (values.effective_date) values.effective_date = values.effective_date.toISOString();
if (editing) {
await updateRegulation(editing.id, values);
message.success('更新成功');
} else {
await createRegulation(values);
message.success('创建成功');
}
setShowModal(false);
form.resetFields();
load();
} catch (e: any) { message.error(e?.detail || '操作失败'); }
};
const handleDelete = async (id: number) => {
try {
await deleteRegulation(id);
message.success('已删除');
load();
} catch (e: any) { message.error(e?.detail || '删除失败'); }
};
const openAdd = () => { setEditing(null); form.resetFields(); setShowModal(true); };
const openEdit = (record: any) => {
setEditing(record);
form.setFieldsValue({
...record,
effective_date: record.effective_date ? dayjs(record.effective_date) : null,
});
setShowModal(true);
};
const columns = [
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '类别', dataIndex: 'category', width: 100, render: (v: string) => REGULATION_CATEGORIES.find(c => c.value === v)?.label || v || '-' },
{ title: '状态', dataIndex: 'status', width: 80, render: (v: string) => {
const s = REGULATION_STATUS.find(s => s.value === v);
return <Tag color={s?.color || 'default'}>{s?.label || v}</Tag>;
}},
{ title: '生效日期', dataIndex: 'effective_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '创建时间', dataIndex: 'created_at', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作', key: 'action', width: 150, render: (_: any, record: any) => (
<Space>
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}></Button>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
)},
];
return (
<>
<Space style={{ marginBottom: 16 }}>
<Select allowClear placeholder="类别" style={{ width: 120 }} options={REGULATION_CATEGORIES}
onChange={v => setFilters(prev => ({ ...prev, category: v, page: 1 }))} />
<Select allowClear placeholder="状态" style={{ width: 100 }} options={REGULATION_STATUS.map(s => ({ label: s.label, value: s.value }))}
onChange={v => setFilters(prev => ({ ...prev, status: v, page: 1 }))} />
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}></Button>
</Space>
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small"
pagination={{ current: filters.page, pageSize: filters.page_size, total: data.total,
showTotal: (t: number) => `${t}`,
onChange: (p, ps) => setFilters(prev => ({ ...prev, page: p, page_size: ps })),
}} />
<Modal title={editing ? '编辑规章制度' : '新增规章制度'} open={showModal}
onCancel={() => { setShowModal(false); form.resetFields(); }}
onOk={() => form.submit()} destroyOnClose width={600}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="category" label="类别"><Select options={REGULATION_CATEGORIES} allowClear /></Form.Item>
<Form.Item name="status" label="状态" initialValue="active">
<Select options={REGULATION_STATUS.map(s => ({ label: s.label, value: s.value }))} />
</Form.Item>
<Form.Item name="effective_date" label="生效日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
<Form.Item name="content" label="内容"><TextArea rows={4} /></Form.Item>
<Form.Item name="attachment_url" label="附件链接"><Input placeholder="附件URL" /></Form.Item>
</Form>
</Modal>
</>
);
}
// ── Standards Tab ────────────────────────────────────────────────────
function StandardsTab() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
const [form] = Form.useForm();
const load = async () => {
setLoading(true);
try {
const res = await getStandards(filters);
setData(res as any);
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, [filters]);
const handleSubmit = async (values: any) => {
try {
if (values.review_date) values.review_date = values.review_date.toISOString();
if (editing) {
await updateStandard(editing.id, values);
message.success('更新成功');
} else {
await createStandard(values);
message.success('创建成功');
}
setShowModal(false);
form.resetFields();
load();
} catch (e: any) { message.error(e?.detail || '操作失败'); }
};
const handleDelete = async (id: number) => {
try {
await deleteStandard(id);
message.success('已删除');
load();
} catch (e: any) { message.error(e?.detail || '删除失败'); }
};
const openAdd = () => { setEditing(null); form.resetFields(); setShowModal(true); };
const openEdit = (record: any) => {
setEditing(record);
form.setFieldsValue({
...record,
review_date: record.review_date ? dayjs(record.review_date) : null,
});
setShowModal(true);
};
const columns = [
{ title: '标准名称', dataIndex: 'name', width: 200, ellipsis: true },
{ title: '标准编号', dataIndex: 'code', width: 140 },
{ title: '类型', dataIndex: 'type', width: 100, render: (v: string) => STANDARD_TYPES.find(t => t.value === v)?.label || v || '-' },
{ title: '合规状态', dataIndex: 'compliance_status', width: 100, render: (v: string) => {
const s = COMPLIANCE_STATUS.find(s => s.value === v);
return <Tag color={s?.color || 'default'}>{s?.label || v}</Tag>;
}},
{ title: '审核日期', dataIndex: 'review_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作', key: 'action', width: 150, render: (_: any, record: any) => (
<Space>
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}></Button>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
)},
];
return (
<>
<Space style={{ marginBottom: 16 }}>
<Select allowClear placeholder="类型" style={{ width: 120 }} options={STANDARD_TYPES}
onChange={v => setFilters(prev => ({ ...prev, type: v, page: 1 }))} />
<Select allowClear placeholder="合规状态" style={{ width: 120 }} options={COMPLIANCE_STATUS.map(s => ({ label: s.label, value: s.value }))}
onChange={v => setFilters(prev => ({ ...prev, compliance_status: v, page: 1 }))} />
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}></Button>
</Space>
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small"
pagination={{ current: filters.page, pageSize: filters.page_size, total: data.total,
showTotal: (t: number) => `${t}`,
onChange: (p, ps) => setFilters(prev => ({ ...prev, page: p, page_size: ps })),
}} />
<Modal title={editing ? '编辑标准规范' : '新增标准规范'} open={showModal}
onCancel={() => { setShowModal(false); form.resetFields(); }}
onOk={() => form.submit()} destroyOnClose width={600}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="name" label="标准名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="code" label="标准编号"><Input placeholder="e.g. ISO 50001, GB/T 23331" /></Form.Item>
<Form.Item name="type" label="类型"><Select options={STANDARD_TYPES} allowClear /></Form.Item>
<Form.Item name="compliance_status" label="合规状态" initialValue="pending">
<Select options={COMPLIANCE_STATUS.map(s => ({ label: s.label, value: s.value }))} />
</Form.Item>
<Form.Item name="review_date" label="审核日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
<Form.Item name="description" label="描述"><TextArea rows={3} /></Form.Item>
</Form>
</Modal>
</>
);
}
// ── Process Docs Tab ─────────────────────────────────────────────────
function ProcessDocsTab() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
const [form] = Form.useForm();
const load = async () => {
setLoading(true);
try {
const res = await getProcessDocs(filters);
setData(res as any);
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, [filters]);
const handleSubmit = async (values: any) => {
try {
if (values.effective_date) values.effective_date = values.effective_date.toISOString();
if (editing) {
await updateProcessDoc(editing.id, values);
message.success('更新成功');
} else {
await createProcessDoc(values);
message.success('创建成功');
}
setShowModal(false);
form.resetFields();
load();
} catch (e: any) { message.error(e?.detail || '操作失败'); }
};
const handleDelete = async (id: number) => {
try {
await deleteProcessDoc(id);
message.success('已删除');
load();
} catch (e: any) { message.error(e?.detail || '删除失败'); }
};
const openAdd = () => { setEditing(null); form.resetFields(); setShowModal(true); };
const openEdit = (record: any) => {
setEditing(record);
form.setFieldsValue({
...record,
effective_date: record.effective_date ? dayjs(record.effective_date) : null,
});
setShowModal(true);
};
const columns = [
{ title: '文档标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '类别', dataIndex: 'category', width: 100, render: (v: string) => PROCESS_CATEGORIES.find(c => c.value === v)?.label || v || '-' },
{ title: '版本', dataIndex: 'version', width: 80 },
{ title: '审批人', dataIndex: 'approved_by', width: 100 },
{ title: '生效日期', dataIndex: 'effective_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作', key: 'action', width: 150, render: (_: any, record: any) => (
<Space>
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}></Button>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
)},
];
return (
<>
<Space style={{ marginBottom: 16 }}>
<Select allowClear placeholder="类别" style={{ width: 120 }} options={PROCESS_CATEGORIES}
onChange={v => setFilters(prev => ({ ...prev, category: v, page: 1 }))} />
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}></Button>
</Space>
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small"
pagination={{ current: filters.page, pageSize: filters.page_size, total: data.total,
showTotal: (t: number) => `${t}`,
onChange: (p, ps) => setFilters(prev => ({ ...prev, page: p, page_size: ps })),
}} />
<Modal title={editing ? '编辑管理流程' : '新增管理流程'} open={showModal}
onCancel={() => { setShowModal(false); form.resetFields(); }}
onOk={() => form.submit()} destroyOnClose width={600}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="title" label="文档标题" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="category" label="类别"><Select options={PROCESS_CATEGORIES} allowClear /></Form.Item>
<Form.Item name="version" label="版本" initialValue="1.0"><Input /></Form.Item>
<Form.Item name="approved_by" label="审批人"><Input /></Form.Item>
<Form.Item name="effective_date" label="生效日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
<Form.Item name="content" label="内容"><TextArea rows={4} /></Form.Item>
</Form>
</Modal>
</>
);
}
// ── Emergency Plans Tab ──────────────────────────────────────────────
function EmergencyPlansTab() {
const [data, setData] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [viewPlan, setViewPlan] = useState<any>(null);
const [filters, setFilters] = useState<Record<string, any>>({ page: 1, page_size: 20 });
const [form] = Form.useForm();
const load = async () => {
setLoading(true);
try {
const res = await getEmergencyPlans(filters);
setData(res as any);
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, [filters]);
const handleSubmit = async (values: any) => {
try {
if (values.review_date) values.review_date = values.review_date.toISOString();
// Parse steps JSON
if (values.steps_json) {
try {
values.steps = JSON.parse(values.steps_json);
} catch {
message.error('应急步骤JSON格式错误');
return;
}
delete values.steps_json;
}
if (editing) {
await updateEmergencyPlan(editing.id, values);
message.success('更新成功');
} else {
await createEmergencyPlan(values);
message.success('创建成功');
}
setShowModal(false);
form.resetFields();
load();
} catch (e: any) { message.error(e?.detail || '操作失败'); }
};
const handleDelete = async (id: number) => {
try {
await deleteEmergencyPlan(id);
message.success('已删除');
load();
} catch (e: any) { message.error(e?.detail || '删除失败'); }
};
const openAdd = () => { setEditing(null); form.resetFields(); setShowModal(true); };
const openEdit = (record: any) => {
setEditing(record);
form.setFieldsValue({
...record,
review_date: record.review_date ? dayjs(record.review_date) : null,
steps_json: record.steps ? JSON.stringify(record.steps, null, 2) : '',
});
setShowModal(true);
};
const columns = [
{ title: '预案标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '场景', dataIndex: 'scenario', width: 100, render: (v: string) => SCENARIO_TYPES.find(s => s.value === v)?.label || v || '-' },
{ title: '负责人', dataIndex: 'responsible_person', width: 100 },
{ title: '状态', dataIndex: 'is_active', width: 80, render: (v: boolean) => (
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>
)},
{ title: '审核日期', dataIndex: 'review_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作', key: 'action', width: 200, render: (_: any, record: any) => (
<Space>
<Button size="small" type="link" icon={<EyeOutlined />} onClick={() => setViewPlan(record)}></Button>
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}></Button>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
)},
];
return (
<>
<Space style={{ marginBottom: 16 }}>
<Select allowClear placeholder="场景" style={{ width: 120 }} options={SCENARIO_TYPES}
onChange={v => setFilters(prev => ({ ...prev, scenario: v, page: 1 }))} />
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}></Button>
</Space>
<Table columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="small"
pagination={{ current: filters.page, pageSize: filters.page_size, total: data.total,
showTotal: (t: number) => `${t}`,
onChange: (p, ps) => setFilters(prev => ({ ...prev, page: p, page_size: ps })),
}} />
{/* Steps Viewer Modal */}
<Modal title={`应急步骤 - ${viewPlan?.title || ''}`} open={!!viewPlan}
onCancel={() => setViewPlan(null)} footer={null} width={600}>
{viewPlan?.steps && Array.isArray(viewPlan.steps) ? (
<List
dataSource={viewPlan.steps}
renderItem={(step: any, index: number) => (
<List.Item>
<List.Item.Meta
avatar={<Badge count={step.step_number || index + 1} style={{ backgroundColor: '#1890ff' }} />}
title={step.action}
description={
<Space direction="vertical" size={0}>
{step.responsible_person && <span>: {step.responsible_person}</span>}
{step.contact && <span>: {step.contact}</span>}
</Space>
}
/>
</List.Item>
)}
/>
) : (
<p style={{ color: '#999' }}></p>
)}
</Modal>
{/* Add/Edit Modal */}
<Modal title={editing ? '编辑应急预案' : '新增应急预案'} open={showModal}
onCancel={() => { setShowModal(false); form.resetFields(); }}
onOk={() => form.submit()} destroyOnClose width={600}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="title" label="预案标题" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="scenario" label="场景"><Select options={SCENARIO_TYPES} allowClear /></Form.Item>
<Form.Item name="responsible_person" label="负责人"><Input /></Form.Item>
<Form.Item name="review_date" label="审核日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
<Form.Item name="is_active" label="启用" initialValue={true}>
<Select options={[{ label: '启用', value: true }, { label: '停用', value: false }]} />
</Form.Item>
<Form.Item name="steps_json" label="应急步骤(JSON)" help='格式: [{"step_number":1,"action":"...","responsible_person":"...","contact":"..."}]'>
<TextArea rows={5} placeholder='[{"step_number":1,"action":"关闭电源","responsible_person":"张三","contact":"13800138000"}]' />
</Form.Item>
</Form>
</Modal>
</>
);
}
// ── Main Component ───────────────────────────────────────────────────
export default function Management() {
return (
<Card size="small">
<Tabs items={[
{ key: 'regulations', label: '规章制度', children: <RegulationsTab /> },
{ key: 'standards', label: '标准规范', children: <StandardsTab /> },
{ key: 'process-docs', label: '管理流程', children: <ProcessDocsTab /> },
{ key: 'emergency-plans', label: '应急预案', children: <EmergencyPlansTab /> },
]} />
</Card>
);
}

View File

@@ -0,0 +1,263 @@
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber, Space, Progress, Row, Col, message } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import { getQuotas, createQuota, updateQuota, deleteQuota, getQuotaCompliance } from '../../services/api';
const energyTypeMap: Record<string, string> = {
electricity: '电力', heat: '热能', water: '水', gas: '燃气',
};
const targetTypeMap: Record<string, string> = {
building: '建筑', department: '部门', device_group: '设备组',
};
const periodMap: Record<string, string> = {
monthly: '月度', yearly: '年度',
};
const statusMap: Record<string, { color: string; text: string }> = {
normal: { color: 'green', text: '正常' },
warning: { color: 'orange', text: '预警' },
exceeded: { color: 'red', text: '超标' },
};
export default function Quota() {
const [quotas, setQuotas] = useState<any[]>([]);
const [compliance, setCompliance] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editingQuota, setEditingQuota] = useState<any>(null);
const [form] = Form.useForm();
useEffect(() => { loadData(); }, []);
const loadData = async () => {
setLoading(true);
try {
const [q, c] = await Promise.all([getQuotas(), getQuotaCompliance()]);
setQuotas(q as any[]);
setCompliance(c);
} catch { message.error('加载配额数据失败'); }
finally { setLoading(false); }
};
const handleCreate = async (values: any) => {
try {
if (editingQuota) {
await updateQuota(editingQuota.id, values);
message.success('配额更新成功');
} else {
await createQuota(values);
message.success('配额创建成功');
}
setShowModal(false);
setEditingQuota(null);
form.resetFields();
loadData();
} catch { message.error('操作失败'); }
};
const handleEdit = (record: any) => {
setEditingQuota(record);
form.setFieldsValue(record);
setShowModal(true);
};
const handleDelete = async (id: number) => {
try {
await deleteQuota(id);
message.success('已删除');
loadData();
} catch { message.error('删除失败'); }
};
// Tab 1: 配置表格
const configColumns = [
{ title: '名称', dataIndex: 'name' },
{ title: '目标类型', dataIndex: 'target_type', render: (v: string) => targetTypeMap[v] || v },
{ title: '能源类型', dataIndex: 'energy_type', render: (v: string) => energyTypeMap[v] || v },
{ title: '周期', dataIndex: 'period', render: (v: string) => periodMap[v] || v },
{ title: '配额值', dataIndex: 'quota_value', render: (v: number, r: any) => `${v} ${r.unit}` },
{ title: '预警%', dataIndex: 'warning_threshold_pct' },
{ title: '告警%', dataIndex: 'alert_threshold_pct' },
{ title: '使用率', dataIndex: 'usage_rate_pct', render: (v: number) => `${(v || 0).toFixed(1)}%` },
{ title: '状态', dataIndex: 'usage_status', render: (s: string) => {
const st = statusMap[s] || { color: 'default', text: s || '正常' };
return <Tag color={st.color}>{st.text}</Tag>;
}},
{ title: '操作', key: 'action', width: 140, render: (_: any, r: any) => (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(r)}></Button>
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r.id)}></Button>
</Space>
)},
];
const configTab = (
<Card size="small" extra={
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingQuota(null); form.resetFields(); setShowModal(true); }}>
</Button>
}>
<Table columns={configColumns} dataSource={quotas} rowKey="id"
loading={loading} size="small" pagination={{ pageSize: 15 }} />
</Card>
);
// Tab 2: 监控进度条
const getProgressColor = (pct: number, warning: number, alert: number) => {
if (pct >= alert) return '#f5222d';
if (pct >= warning) return '#faad14';
return '#52c41a';
};
const monitorTab = (
<Row gutter={[16, 16]}>
{quotas.map(q => (
<Col xs={24} sm={12} md={8} key={q.id}>
<Card size="small" title={q.name}
extra={<Tag color={statusMap[q.usage_status]?.color || 'default'}>{statusMap[q.usage_status]?.text || '正常'}</Tag>}>
<div style={{ marginBottom: 8, color: '#666', fontSize: 13 }}>
{energyTypeMap[q.energy_type] || q.energy_type} | {periodMap[q.period] || q.period}
</div>
<Progress
percent={Math.min(q.usage_rate_pct || 0, 100)}
strokeColor={getProgressColor(q.usage_rate_pct || 0, q.warning_threshold_pct, q.alert_threshold_pct)}
format={() => `${(q.usage_rate_pct || 0).toFixed(1)}%`}
/>
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
{(q.current_usage || 0).toFixed(1)} / {q.quota_value} {q.unit}
</div>
</Card>
</Col>
))}
{quotas.length === 0 && (
<Col span={24}><Card size="small"><div style={{ textAlign: 'center', color: '#999', padding: 40 }}></div></Card></Col>
)}
</Row>
);
// Tab 3: 分析图表
const complianceChartOption = compliance?.details ? {
tooltip: { trigger: 'axis' },
legend: { data: ['配额', '实际用量'] },
grid: { top: 50, right: 40, bottom: 40, left: 60 },
xAxis: {
type: 'category',
data: compliance.details.map((d: any) => d.name),
axisLabel: { rotate: 30, fontSize: 11 },
},
yAxis: { type: 'value', name: 'kWh' },
series: [
{
name: '配额',
type: 'bar',
data: compliance.details.map((d: any) => d.quota_value),
itemStyle: { color: '#1890ff', opacity: 0.4 },
},
{
name: '实际用量',
type: 'bar',
data: compliance.details.map((d: any) => d.actual_value),
itemStyle: {
color: (params: any) => {
const detail = compliance.details[params.dataIndex];
if (detail.status === 'exceeded') return '#f5222d';
if (detail.status === 'warning') return '#faad14';
return '#52c41a';
},
},
},
],
} : {};
const analysisTab = (
<div>
{compliance?.summary && (
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}>
<Card size="small"><div style={{ textAlign: 'center' }}><div style={{ fontSize: 24, fontWeight: 600 }}>{compliance.summary.total}</div><div style={{ color: '#999' }}></div></div></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><div style={{ textAlign: 'center' }}><div style={{ fontSize: 24, fontWeight: 600, color: '#52c41a' }}>{compliance.summary.normal}</div><div style={{ color: '#999' }}></div></div></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><div style={{ textAlign: 'center' }}><div style={{ fontSize: 24, fontWeight: 600, color: '#faad14' }}>{compliance.summary.warning}</div><div style={{ color: '#999' }}></div></div></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><div style={{ textAlign: 'center' }}><div style={{ fontSize: 24, fontWeight: 600, color: '#f5222d' }}>{compliance.summary.exceeded}</div><div style={{ color: '#999' }}></div></div></Card>
</Col>
</Row>
)}
<Card title="配额合规分析" size="small">
{compliance?.details?.length > 0 ? (
<ReactECharts option={complianceChartOption} style={{ height: 400 }} />
) : (
<div style={{ textAlign: 'center', color: '#999', padding: 60 }}></div>
)}
</Card>
</div>
);
return (
<div>
<Tabs items={[
{ key: 'config', label: '配置', children: configTab },
{ key: 'monitor', label: '监控', children: monitorTab },
{ key: 'analysis', label: '分析', children: analysisTab },
]} />
<Modal
title={editingQuota ? '编辑配额' : '新建配额'}
open={showModal}
onCancel={() => { setShowModal(false); setEditingQuota(null); }}
onOk={() => form.submit()}
okText={editingQuota ? '更新' : '创建'}
cancelText="取消"
>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="name" label="配额名称" rules={[{ required: true }]}>
<Input placeholder="例: 办公楼月度用电配额" />
</Form.Item>
<Form.Item name="target_type" label="目标类型" rules={[{ required: true }]}>
<Select options={[
{ label: '建筑', value: 'building' },
{ label: '部门', value: 'department' },
{ label: '设备组', value: 'device_group' },
]} />
</Form.Item>
<Form.Item name="target_id" label="目标ID" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} min={1} />
</Form.Item>
<Form.Item name="energy_type" label="能源类型" rules={[{ required: true }]}>
<Select options={[
{ label: '电力', value: 'electricity' },
{ label: '热能', value: 'heat' },
{ label: '水', value: 'water' },
{ label: '燃气', value: 'gas' },
]} />
</Form.Item>
<Form.Item name="period" label="周期" rules={[{ required: true }]}>
<Select options={[
{ label: '月度', value: 'monthly' },
{ label: '年度', value: 'yearly' },
]} />
</Form.Item>
<Form.Item name="quota_value" label="配额值" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
<Form.Item name="unit" label="单位" initialValue="kWh">
<Input />
</Form.Item>
<Form.Item name="warning_threshold_pct" label="预警阈值(%)" initialValue={80}>
<InputNumber style={{ width: '100%' }} min={0} max={100} />
</Form.Item>
<Form.Item name="alert_threshold_pct" label="告警阈值(%)" initialValue={95}>
<InputNumber style={{ width: '100%' }} min={0} max={100} />
</Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@@ -125,6 +125,54 @@ export const exportEnergyData = async (params: {
window.URL.revokeObjectURL(url);
};
// Cost
export const getCostPricing = (params?: Record<string, any>) => api.get('/cost/pricing', { params });
export const createCostPricing = (data: any) => api.post('/cost/pricing', data);
export const updateCostPricing = (id: number, data: any) => api.put(`/cost/pricing/${id}`, data);
export const deleteCostPricing = (id: number) => api.delete(`/cost/pricing/${id}`);
export const getCostPricingPeriods = (id: number) => api.get(`/cost/pricing/${id}/periods`);
export const addCostPricingPeriod = (id: number, data: any) => api.post(`/cost/pricing/${id}/periods`, data);
export const getCostSummary = (params: Record<string, any>) => api.get('/cost/summary', { params });
export const getCostComparison = (params?: Record<string, any>) => api.get('/cost/comparison', { params });
export const getCostBreakdown = (params: Record<string, any>) => api.get('/cost/breakdown', { params });
// Quota
export const getQuotas = (params?: Record<string, any>) => api.get('/quota', { params });
export const createQuota = (data: any) => api.post('/quota', data);
export const updateQuota = (id: number, data: any) => api.put(`/quota/${id}`, data);
export const deleteQuota = (id: number) => api.delete(`/quota/${id}`);
export const getQuotaUsage = (params?: Record<string, any>) => api.get('/quota/usage', { params });
export const getQuotaCompliance = () => api.get('/quota/compliance');
// Energy Categories (分项能耗)
export const getEnergyCategories = () => api.get('/energy/categories');
export const createEnergyCategory = (data: any) => api.post('/energy/categories', data);
export const updateEnergyCategory = (id: number, data: any) => api.put(`/energy/categories/${id}`, data);
export const getEnergyByCategory = (params?: Record<string, any>) => api.get('/energy/by-category', { params });
export const getCategoryRanking = (params?: Record<string, any>) => api.get('/energy/category-ranking', { params });
export const getCategoryTrend = (params?: Record<string, any>) => api.get('/energy/category-trend', { params });
// Maintenance (运维管理)
export const getMaintenanceDashboard = () => api.get('/maintenance/dashboard');
export const getMaintenancePlans = (params?: Record<string, any>) => api.get('/maintenance/plans', { params });
export const createMaintenancePlan = (data: any) => api.post('/maintenance/plans', data);
export const updateMaintenancePlan = (id: number, data: any) => api.put(`/maintenance/plans/${id}`, data);
export const deleteMaintenancePlan = (id: number) => api.delete(`/maintenance/plans/${id}`);
export const triggerMaintenancePlan = (id: number) => api.post(`/maintenance/plans/${id}/trigger`);
export const getMaintenanceRecords = (params?: Record<string, any>) => api.get('/maintenance/records', { params });
export const createMaintenanceRecord = (data: any) => api.post('/maintenance/records', data);
export const updateMaintenanceRecord = (id: number, data: any) => api.put(`/maintenance/records/${id}`, data);
export const getMaintenanceOrders = (params?: Record<string, any>) => api.get('/maintenance/orders', { params });
export const createMaintenanceOrder = (data: any) => api.post('/maintenance/orders', data);
export const updateMaintenanceOrder = (id: number, data: any) => api.put(`/maintenance/orders/${id}`, data);
export const assignMaintenanceOrder = (id: number, userId: number) => api.put(`/maintenance/orders/${id}/assign?assigned_to=${userId}`);
export const completeMaintenanceOrder = (id: number, resolution = '', actualCost?: number) =>
api.put(`/maintenance/orders/${id}/complete?resolution=${encodeURIComponent(resolution)}${actualCost !== undefined ? `&actual_cost=${actualCost}` : ''}`);
export const getMaintenanceDuty = (params?: Record<string, any>) => api.get('/maintenance/duty', { params });
export const createMaintenanceDuty = (data: any) => api.post('/maintenance/duty', data);
export const updateMaintenanceDuty = (id: number, data: any) => api.put(`/maintenance/duty/${id}`, data);
export const deleteMaintenanceDuty = (id: number) => api.delete(`/maintenance/duty/${id}`);
// Users
export const getUsers = (params?: Record<string, any>) => api.get('/users', { params });
export const createUser = (data: any) => api.post('/users', data);
@@ -138,4 +186,91 @@ export const getAuditLogs = (params?: Record<string, any>) => api.get('/audit/lo
export const getSettings = () => api.get('/settings');
export const updateSettings = (data: any) => api.put('/settings', data);
// Charging - Stations
export const getChargingStations = (params?: Record<string, any>) => api.get('/charging/stations', { params });
export const createChargingStation = (data: any) => api.post('/charging/stations', data);
export const updateChargingStation = (id: number, data: any) => api.put(`/charging/stations/${id}`, data);
export const deleteChargingStation = (id: number) => api.delete(`/charging/stations/${id}`);
export const getStationPiles = (stationId: number) => api.get(`/charging/stations/${stationId}/piles`);
// Charging - Piles
export const getChargingPiles = (params?: Record<string, any>) => api.get('/charging/piles', { params });
export const createChargingPile = (data: any) => api.post('/charging/piles', data);
export const updateChargingPile = (id: number, data: any) => api.put(`/charging/piles/${id}`, data);
export const deleteChargingPile = (id: number) => api.delete(`/charging/piles/${id}`);
// Charging - Pricing
export const getChargingPricing = (params?: Record<string, any>) => api.get('/charging/pricing', { params });
export const createChargingPricing = (data: any) => api.post('/charging/pricing', data);
export const updateChargingPricing = (id: number, data: any) => api.put(`/charging/pricing/${id}`, data);
export const deleteChargingPricing = (id: number) => api.delete(`/charging/pricing/${id}`);
// Charging - Orders
export const getChargingOrders = (params?: Record<string, any>) => api.get('/charging/orders', { params });
export const getChargingRealtimeOrders = () => api.get('/charging/orders/realtime');
export const getChargingAbnormalOrders = (params?: Record<string, any>) => api.get('/charging/orders/abnormal', { params });
export const getChargingOrder = (id: number) => api.get(`/charging/orders/${id}`);
export const settleChargingOrder = (id: number) => api.post(`/charging/orders/${id}/settle`);
// Charging - Dashboard
export const getChargingDashboard = () => api.get('/charging/dashboard');
// Charging - Merchants
export const getChargingMerchants = () => api.get('/charging/merchants');
export const createChargingMerchant = (data: any) => api.post('/charging/merchants', data);
export const updateChargingMerchant = (id: number, data: any) => api.put(`/charging/merchants/${id}`, data);
export const deleteChargingMerchant = (id: number) => api.delete(`/charging/merchants/${id}`);
// Charging - Brands
export const getChargingBrands = () => api.get('/charging/brands');
export const createChargingBrand = (data: any) => api.post('/charging/brands', data);
export const updateChargingBrand = (id: number, data: any) => api.put(`/charging/brands/${id}`, data);
export const deleteChargingBrand = (id: number) => api.delete(`/charging/brands/${id}`);
// Energy Analysis (Loss / YoY / MoM)
export const getEnergyLoss = (params: Record<string, any>) => api.get('/energy/loss', { params });
export const getEnergyYoy = (params?: Record<string, any>) => api.get('/energy/yoy', { params });
export const getEnergyMom = (params?: Record<string, any>) => api.get('/energy/mom', { params });
// Alarm Analytics
export const getAlarmAnalytics = (params?: Record<string, any>) => api.get('/alarms/analytics', { params });
export const getTopAlarmDevices = (params?: Record<string, any>) => api.get('/alarms/top-devices', { params });
export const getAlarmMttr = (params?: Record<string, any>) => api.get('/alarms/mttr', { params });
export const toggleAlarmRule = (ruleId: number) => api.put(`/alarms/rules/${ruleId}/toggle`);
export const getAlarmRuleHistory = (ruleId: number, params?: Record<string, any>) =>
api.get(`/alarms/rules/${ruleId}/history`, { params });
// Data Query (电参量查询)
export const queryElectricalParams = (params: Record<string, any>) => api.get('/energy/params', { params });
// Device Topology (设备拓扑)
export const getDeviceTopology = () => api.get('/devices/topology');
// Management - Regulations (规章制度)
export const getRegulations = (params?: Record<string, any>) => api.get('/management/regulations', { params });
export const createRegulation = (data: any) => api.post('/management/regulations', data);
export const updateRegulation = (id: number, data: any) => api.put(`/management/regulations/${id}`, data);
export const deleteRegulation = (id: number) => api.delete(`/management/regulations/${id}`);
// Management - Standards (标准规范)
export const getStandards = (params?: Record<string, any>) => api.get('/management/standards', { params });
export const createStandard = (data: any) => api.post('/management/standards', data);
export const updateStandard = (id: number, data: any) => api.put(`/management/standards/${id}`, data);
export const deleteStandard = (id: number) => api.delete(`/management/standards/${id}`);
// Management - Process Docs (管理流程)
export const getProcessDocs = (params?: Record<string, any>) => api.get('/management/process-docs', { params });
export const createProcessDoc = (data: any) => api.post('/management/process-docs', data);
export const updateProcessDoc = (id: number, data: any) => api.put(`/management/process-docs/${id}`, data);
export const deleteProcessDoc = (id: number) => api.delete(`/management/process-docs/${id}`);
// Management - Emergency Plans (应急预案)
export const getEmergencyPlans = (params?: Record<string, any>) => api.get('/management/emergency-plans', { params });
export const createEmergencyPlan = (data: any) => api.post('/management/emergency-plans', data);
export const updateEmergencyPlan = (id: number, data: any) => api.put(`/management/emergency-plans/${id}`, data);
export const deleteEmergencyPlan = (id: number) => api.delete(`/management/emergency-plans/${id}`);
// Management - Compliance
export const getComplianceOverview = () => api.get('/management/compliance');
export default api;

View File

@@ -7,7 +7,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
target: 'http://localhost:8088',
changeOrigin: true,
},
},