feat: v2.0 — maintenance module, AI analysis, station power fix

- Add full 检修维护中心 (6.4): 3-type work orders (消缺/巡检/抄表),
  asset management, warehouse, work plans, billing settlement
- Add AI智能分析 tab with LLM-powered diagnostics (StepFun + ZhipuAI)
- Add AI模型配置 settings page (provider, temperature, prompts)
- Fix station power accuracy: use API station total (station_power)
  instead of inverter-level computation — eliminates timing gaps
- Add 7 new DB models, 4 new API routers, 5 new frontend pages
- Migrations: 009 (maintenance expansion) + 010 (AI analysis)
- Version bump: 1.6.1 → 2.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-12 21:16:03 +08:00
parent 7947a230c4
commit f0f13faf00
30 changed files with 3325 additions and 52 deletions

View File

@@ -19,6 +19,10 @@ import SystemManagement from './pages/System';
import Quota from './pages/Quota';
import Maintenance from './pages/Maintenance';
import AssetManagement from './pages/AssetManagement';
import WarehouseManagement from './pages/WarehouseManagement';
import WorkPlanManagement from './pages/WorkPlanManagement';
import BillingManagement from './pages/BillingManagement';
import DataQuery from './pages/DataQuery';
import Management from './pages/Management';
import Prediction from './pages/Prediction';
@@ -67,6 +71,10 @@ function AppContent() {
<Route path="quota" element={<Quota />} />
<Route path="maintenance" element={<Maintenance />} />
<Route path="asset-management" element={<AssetManagement />} />
<Route path="warehouse" element={<WarehouseManagement />} />
<Route path="work-plans" element={<WorkPlanManagement />} />
<Route path="billing" element={<BillingManagement />} />
<Route path="data-query" element={<DataQuery />} />
<Route path="management" element={<Management />} />
<Route path="prediction" element={<Prediction />} />

View File

@@ -8,7 +8,8 @@ import {
InfoCircleOutlined, FundProjectionScreenOutlined, GlobalOutlined,
BulbOutlined, BulbFilled, FundOutlined, CarOutlined, ToolOutlined,
SearchOutlined, SolutionOutlined, RobotOutlined, ExperimentOutlined, ApartmentOutlined, ScanOutlined,
DollarOutlined, BookOutlined,
DollarOutlined, BookOutlined, DatabaseOutlined, ShopOutlined,
ScheduleOutlined, AccountBookOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -70,7 +71,15 @@ export default function MainLayout() {
{ 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: 'maintenance-group', icon: <ToolOutlined />, label: '检修维护中心',
children: [
{ key: '/maintenance', icon: <ToolOutlined />, label: '任务工作台' },
{ key: '/asset-management', icon: <DatabaseOutlined />, label: '设备资产' },
{ key: '/warehouse', icon: <ShopOutlined />, label: '仓库管理' },
{ key: '/work-plans', icon: <ScheduleOutlined />, label: '工作计划' },
{ key: '/billing', icon: <AccountBookOutlined />, label: '电费结算' },
],
},
{ key: '/data-query', icon: <SearchOutlined />, label: t('menu.dataQuery', '数据查询') },
{ key: '/prediction', icon: <RobotOutlined />, label: t('menu.prediction', 'AI预测') },
{ key: '/management', icon: <SolutionOutlined />, label: t('menu.management', '管理体系') },

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import {
Card, Row, Col, Statistic, Tag, Tabs, Button, Table, Space, Progress,
Drawer, Descriptions, Timeline, Badge, Select, message, Tooltip, Empty,
Modal, List, Calendar, Input,
Modal, List, Calendar, Input, Radio, InputNumber, Spin,
} from 'antd';
import {
RobotOutlined, HeartOutlined, AlertOutlined, MedicineBoxOutlined,
@@ -20,6 +20,7 @@ import {
getAiOpsDiagnostics, runDeviceDiagnostics,
getAiOpsPredictions, getAiOpsMaintenanceSchedule,
getAiOpsInsights, triggerInsights, triggerHealthCalc, triggerPredictions,
aiAnalyze, getAiAnalysisHistory,
} from '../../services/api';
const severityColors: Record<string, string> = {
@@ -754,6 +755,105 @@ function InsightsBoard() {
);
}
// ── Tab: AI Analysis ──────────────────────────────────────────────
function AIAnalysisTab() {
const [scope, setScope] = useState<string>('station');
const [deviceId, setDeviceId] = useState<number | undefined>();
const [analyzing, setAnalyzing] = useState(false);
const [result, setResult] = useState<any>(null);
const [history, setHistory] = useState<any>({ total: 0, items: [] });
const [historyLoading, setHistoryLoading] = useState(false);
const loadHistory = async () => {
setHistoryLoading(true);
try {
setHistory(await getAiAnalysisHistory({ page_size: 5 }));
} catch { /* ignore */ }
finally { setHistoryLoading(false); }
};
useEffect(() => { loadHistory(); }, []);
const handleAnalyze = async () => {
setAnalyzing(true);
setResult(null);
try {
const params: any = { scope };
if (scope === 'device' && deviceId) params.device_id = deviceId;
const res = await aiAnalyze(params);
setResult(res);
loadHistory();
} catch (e: any) {
message.error(e?.detail || 'AI分析失败请检查AI设置');
}
finally { setAnalyzing(false); }
};
return (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<Space size="large" wrap>
<span></span>
<Radio.Group value={scope} onChange={(e) => setScope(e.target.value)}>
<Radio.Button value="station"></Radio.Button>
<Radio.Button value="device"></Radio.Button>
</Radio.Group>
{scope === 'device' && (
<InputNumber placeholder="设备ID" value={deviceId} onChange={(v) => setDeviceId(v || undefined)} min={1} style={{ width: 120 }} />
)}
<Button type="primary" icon={<ThunderboltOutlined />} loading={analyzing} onClick={handleAnalyze}>
</Button>
</Space>
</Card>
{analyzing && (
<Card size="small" style={{ marginBottom: 16, textAlign: 'center', padding: 40 }}>
<Spin size="large" />
<div style={{ marginTop: 16, color: '#999' }}>AI正在分析中...</div>
</Card>
)}
{result && !analyzing && (
<Card size="small" title={
<Space>
<Tag color="blue">AI分析结果</Tag>
<span style={{ fontSize: 12, color: '#999' }}>: {result.model} | : {result.duration_ms}ms</span>
</Space>
} style={{ marginBottom: 16 }}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', margin: 0, lineHeight: 1.8 }}>
{result.result}
</pre>
</Card>
)}
<Card size="small" title="分析历史" loading={historyLoading}>
{history.items.length === 0 ? (
<Empty description="暂无分析记录" />
) : (
<List size="small" dataSource={history.items} renderItem={(item: any) => (
<List.Item>
<List.Item.Meta
title={<Space>
<Tag color={item.scope === 'device' ? 'blue' : 'green'}>{item.scope === 'device' ? `设备 #${item.device_id}` : '全站'}</Tag>
<span style={{ fontSize: 12, color: '#999' }}>{item.model_used} | {item.duration_ms}ms</span>
</Space>}
description={
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}>{item.created_at}</div>
<div style={{ fontSize: 13 }}>{item.result_text}</div>
</div>
}
/>
</List.Item>
)} />
)}
</Card>
</div>
);
}
// ── Main Page ──────────────────────────────────────────────────────
export default function AIOperations() {
@@ -851,6 +951,11 @@ export default function AIOperations() {
label: <span><BulbOutlined /> </span>,
children: <InsightsBoard />,
},
{
key: 'ai-analysis',
label: <span><ThunderboltOutlined /> AI智能分析</span>,
children: <AIAnalysisTab />,
},
]}
/>
</Card>

View File

@@ -0,0 +1,277 @@
import { useEffect, useState } from 'react';
import {
Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select,
Space, message, Row, Col, Statistic, DatePicker,
} from 'antd';
import { PlusOutlined, DatabaseOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import {
getAssets, createAsset, getAssetChanges, getAssetCategories,
createAssetCategory, getAssetStats,
} from '../../services/api';
const statusMap: Record<string, { color: string; text: string }> = {
active: { color: 'green', text: '在用' },
inactive: { color: 'default', text: '闲置' },
scrapped: { color: 'red', text: '报废' },
under_repair: { color: 'orange', text: '维修中' },
};
const changeTypeMap: Record<string, { color: string; text: string }> = {
purchase: { color: 'green', text: '采购入库' },
transfer: { color: 'blue', text: '调拨' },
repair: { color: 'orange', text: '维修' },
scrap: { color: 'red', text: '报废' },
inventory: { color: 'default', text: '盘点' },
};
// ── Tab 1: Asset Cards ────────────────────────────────────────────
function AssetCardsTab() {
const [assets, setAssets] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [keyword, setKeyword] = useState('');
const [showModal, setShowModal] = useState(false);
const [detailModal, setDetailModal] = useState<{ open: boolean; asset: any }>({ open: false, asset: null });
const [form] = Form.useForm();
const [stats, setStats] = useState<any>(null);
const loadAssets = async () => {
setLoading(true);
try {
setAssets(await getAssets({ keyword: keyword || undefined }));
} catch { message.error('加载资产列表失败'); }
finally { setLoading(false); }
};
useEffect(() => {
(async () => {
try { setStats(await getAssetStats()); } catch {}
})();
}, []);
useEffect(() => { loadAssets(); }, [keyword]);
const handleCreate = async (values: any) => {
try {
if (values.purchase_date) values.purchase_date = values.purchase_date.format('YYYY-MM-DD');
await createAsset(values);
message.success('资产创建成功');
setShowModal(false);
form.resetFields();
loadAssets();
} catch { message.error('创建失败'); }
};
const columns = [
{ title: '资产编码', dataIndex: 'asset_code', width: 140 },
{ title: '名称', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category_name', width: 100 },
{ title: '型号', dataIndex: 'model_number', width: 120 },
{ title: '制造商', dataIndex: 'manufacturer', width: 120 },
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => {
const s = statusMap[v] || { color: 'default', text: v };
return <Tag color={s.color}>{s.text}</Tag>;
}},
{ title: '位置', dataIndex: 'location', width: 120 },
{ title: '购入日期', dataIndex: 'purchase_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
];
return (
<div>
{stats && (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}><Card><Statistic title="资产总数" value={stats.total || 0} prefix={<DatabaseOutlined />} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="在用" value={stats.active || 0} valueStyle={{ color: '#52c41a' }} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="维修中" value={stats.under_repair || 0} valueStyle={{ color: '#fa8c16' }} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="已报废" value={stats.scrapped || 0} valueStyle={{ color: '#f5222d' }} /></Card></Col>
</Row>
)}
<Card size="small" extra={
<Space>
<Input.Search placeholder="搜索资产" allowClear onSearch={setKeyword} style={{ width: 200 }} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>
</Space>
}>
<Table columns={columns} dataSource={Array.isArray(assets) ? assets : (assets.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: assets.total, pageSize: 20 }}
onRow={(record) => ({ onClick: () => setDetailModal({ open: true, asset: record }), style: { cursor: 'pointer' } })}
/>
</Card>
<Modal title="新增资产" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消" width={600}>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="asset_code" label="资产编码" rules={[{ required: true }]}>
<Input placeholder="例: AST-2026-001" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="name" label="资产名称" rules={[{ required: true }]}>
<Input placeholder="例: 空气源热泵" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="category_id" label="分类">
<Input placeholder="分类ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="model_number" label="型号">
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="manufacturer" label="制造商">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="状态" initialValue="active">
<Select options={Object.entries(statusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="location" label="安装位置">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="purchase_date" label="购入日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal title="资产详情" open={detailModal.open} onCancel={() => setDetailModal({ open: false, asset: null })} footer={null} width={500}>
{detailModal.asset && (
<div>
<p><strong>:</strong> {detailModal.asset.asset_code}</p>
<p><strong>:</strong> {detailModal.asset.name}</p>
<p><strong>:</strong> {detailModal.asset.category_name || '-'}</p>
<p><strong>:</strong> {detailModal.asset.model_number || '-'}</p>
<p><strong>:</strong> {detailModal.asset.manufacturer || '-'}</p>
<p><strong>:</strong> {statusMap[detailModal.asset.status]?.text || detailModal.asset.status}</p>
<p><strong>:</strong> {detailModal.asset.location || '-'}</p>
<p><strong>:</strong> {detailModal.asset.purchase_date ? dayjs(detailModal.asset.purchase_date).format('YYYY-MM-DD') : '-'}</p>
</div>
)}
</Modal>
</div>
);
}
// ── Tab 2: Asset Changes ──────────────────────────────────────────
function AssetChangesTab() {
const [changes, setChanges] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [typeFilter, setTypeFilter] = useState<string | undefined>();
const loadChanges = async () => {
setLoading(true);
try {
setChanges(await getAssetChanges({ change_type: typeFilter }));
} catch { message.error('加载资产变动失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadChanges(); }, [typeFilter]);
const columns = [
{ title: '资产编码', dataIndex: 'asset_code', width: 140 },
{ title: '变动类型', dataIndex: 'change_type', width: 100, render: (v: string) => {
const c = changeTypeMap[v] || { color: 'default', text: v };
return <Tag color={c.color}>{c.text}</Tag>;
}},
{ title: '描述', dataIndex: 'description' },
{ title: '变动日期', dataIndex: 'change_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作人', dataIndex: 'operator', width: 100 },
];
return (
<Card size="small" extra={
<Select allowClear placeholder="变动类型" style={{ width: 140 }} value={typeFilter} onChange={setTypeFilter}
options={Object.entries(changeTypeMap).map(([k, v]) => ({ label: v.text, value: k }))} />
}>
<Table columns={columns} dataSource={Array.isArray(changes) ? changes : (changes.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: changes.total, pageSize: 20 }} />
</Card>
);
}
// ── Tab 3: Asset Categories ───────────────────────────────────────
function CategoriesTab() {
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [form] = Form.useForm();
const loadCategories = async () => {
setLoading(true);
try { setCategories(await getAssetCategories() as any[]); }
catch { message.error('加载资产分类失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadCategories(); }, []);
const handleCreate = async (values: any) => {
try {
await createAssetCategory(values);
message.success('分类创建成功');
setShowModal(false);
form.resetFields();
loadCategories();
} catch { message.error('创建失败'); }
};
const columns = [
{ title: '分类名称', dataIndex: 'name' },
{ title: '上级分类', dataIndex: 'parent_name', render: (v: string) => v || '-' },
{ title: '描述', dataIndex: 'description', render: (v: string) => v || '-' },
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Table columns={columns} dataSource={categories} 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="parent_id" label="上级分类ID">
<Input placeholder="留空表示顶级分类" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Main Component ────────────────────────────────────────────────
export default function AssetManagement() {
return (
<div>
<Tabs items={[
{ key: 'cards', label: '资产卡片', children: <AssetCardsTab /> },
{ key: 'changes', label: '资产变动', children: <AssetChangesTab /> },
{ key: 'categories', label: '资产分类', children: <CategoriesTab /> },
]} />
</div>
);
}

View File

@@ -0,0 +1,189 @@
import { useEffect, useState } from 'react';
import {
Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber,
Space, message, Row, Col, Statistic, DatePicker,
} from 'antd';
import { PlusOutlined, DollarOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { getBillingRecords, createBillingRecord, getBillingStats } from '../../services/api';
const billingTypeMap: Record<string, string> = {
self_use: '自发自用',
surplus_grid: '余额上网',
full_grid: '全额上网',
};
const billingStatusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'default', text: '待录入' },
entered: { color: 'blue', text: '已录入' },
invoiced: { color: 'green', text: '已开票' },
};
// ── Tab 1: Billing List ───────────────────────────────────────────
function BillingListTab() {
const [records, setRecords] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [typeFilter, setTypeFilter] = useState<string | undefined>();
const [statusFilter, setStatusFilter] = useState<string | undefined>();
const [yearFilter, setYearFilter] = useState<number | undefined>();
const [form] = Form.useForm();
const loadRecords = async () => {
setLoading(true);
try {
setRecords(await getBillingRecords({
billing_type: typeFilter,
status: statusFilter,
year: yearFilter,
}));
} catch { message.error('加载结算记录失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadRecords(); }, [typeFilter, statusFilter, yearFilter]);
const handleCreate = async (values: any) => {
try {
if (values.billing_period) values.billing_period = values.billing_period.format('YYYY-MM');
await createBillingRecord(values);
message.success('结算记录创建成功');
setShowModal(false);
form.resetFields();
loadRecords();
} catch { message.error('创建失败'); }
};
const columns = [
{ title: '场站', dataIndex: 'station_name', width: 140 },
{ title: '结算类型', dataIndex: 'billing_type', width: 100, render: (v: string) => (
<Tag>{billingTypeMap[v] || v}</Tag>
)},
{ title: '结算周期', dataIndex: 'billing_period', width: 100, render: (v: string) => v ? dayjs(v).format('YYYY-MM') : '-' },
{ title: '发电量(kWh)', dataIndex: 'generation_kwh', width: 120, render: (v: number) => v != null ? v.toFixed(1) : '-' },
{ title: '自用量(kWh)', dataIndex: 'self_use_kwh', width: 120, render: (v: number) => v != null ? v.toFixed(1) : '-' },
{ title: '上网量(kWh)', dataIndex: 'grid_feed_kwh', width: 120, render: (v: number) => v != null ? v.toFixed(1) : '-' },
{ title: '总金额', dataIndex: 'total_amount', width: 110, render: (v: number) => v != null ? `¥${v.toFixed(2)}` : '-' },
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => {
const s = billingStatusMap[v] || { color: 'default', text: v };
return <Tag color={s.color}>{s.text}</Tag>;
}},
];
const currentYear = dayjs().year();
const yearOptions = Array.from({ length: 5 }, (_, i) => ({ label: `${currentYear - i}`, value: currentYear - i }));
return (
<Card size="small" extra={
<Space>
<Select allowClear placeholder="结算类型" style={{ width: 120 }} value={typeFilter} onChange={setTypeFilter}
options={Object.entries(billingTypeMap).map(([k, v]) => ({ label: v, value: k }))} />
<Select allowClear placeholder="状态" style={{ width: 100 }} value={statusFilter} onChange={setStatusFilter}
options={Object.entries(billingStatusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
<Select allowClear placeholder="年份" style={{ width: 100 }} value={yearFilter} onChange={setYearFilter}
options={yearOptions} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>
</Space>
}>
<Table columns={columns} dataSource={Array.isArray(records) ? records : (records.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: records.total, pageSize: 20 }} />
<Modal title="新增结算记录" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消" width={600}>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="station_name" label="场站名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="billing_type" label="结算类型" initialValue="self_use" rules={[{ required: true }]}>
<Select options={Object.entries(billingTypeMap).map(([k, v]) => ({ label: v, value: k }))} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="billing_period" label="结算周期" rules={[{ required: true }]}>
<DatePicker picker="month" style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="状态" initialValue="pending">
<Select options={Object.entries(billingStatusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="generation_kwh" label="发电量(kWh)">
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="self_use_kwh" label="自用量(kWh)">
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="grid_feed_kwh" label="上网量(kWh)">
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
</Row>
<Form.Item name="total_amount" label="总金额">
<InputNumber style={{ width: '100%' }} min={0} prefix="¥" />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Tab 2: Billing Stats ──────────────────────────────────────────
function BillingStatsTab() {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try { setStats(await getBillingStats()); }
catch { message.error('加载结算统计失败'); }
finally { setLoading(false); }
})();
}, []);
return (
<div>
<Row gutter={[16, 16]}>
<Col xs={12} sm={6}>
<Card loading={loading}><Statistic title="总发电量(kWh)" value={stats?.total_generation || 0} precision={1} prefix={<DollarOutlined />} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card loading={loading}><Statistic title="总金额" value={stats?.total_amount || 0} precision={2} prefix="¥" /></Card>
</Col>
<Col xs={12} sm={6}>
<Card loading={loading}><Statistic title="自用金额" value={stats?.self_use_amount || 0} precision={2} prefix="¥" /></Card>
</Col>
<Col xs={12} sm={6}>
<Card loading={loading}><Statistic title="上网收入" value={stats?.grid_feed_amount || 0} precision={2} prefix="¥" valueStyle={{ color: '#52c41a' }} /></Card>
</Col>
</Row>
</div>
);
}
// ── Main Component ────────────────────────────────────────────────
export default function BillingManagement() {
return (
<div>
<Tabs items={[
{ key: 'list', label: '结算列表', children: <BillingListTab /> },
{ key: 'stats', label: '结算统计', children: <BillingStatsTab /> },
]} />
</div>
);
}

View File

@@ -15,6 +15,12 @@ import {
getMaintenanceDuty, createMaintenanceDuty,
} from '../../services/api';
const orderTypeMap: Record<string, { label: string; color: string }> = {
repair: { label: '消缺工单', color: 'red' },
inspection: { label: '巡检工单', color: 'blue' },
meter_reading: { label: '抄表工单', color: 'green' },
};
const priorityMap: Record<string, { color: string; text: string }> = {
critical: { color: 'red', text: '紧急' },
high: { color: 'orange', text: '高' },
@@ -59,6 +65,10 @@ function DashboardTab() {
const orderColumns = [
{ title: '工单号', dataIndex: 'code', width: 160 },
{ title: '类型', dataIndex: 'order_type', width: 100, render: (v: string) => {
const t = orderTypeMap[v] || { label: v || '消缺', color: 'default' };
return <Tag color={t.color}>{t.label}</Tag>;
}},
{ title: '标题', dataIndex: 'title' },
{ title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => {
const p = priorityMap[v] || { color: 'default', text: v };
@@ -213,21 +223,26 @@ function OrdersTab() {
const [orders, setOrders] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [typeFilter, setTypeFilter] = useState<string | undefined>();
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({})); }
try { setOrders(await getMaintenanceOrders({ order_type: typeFilter })); }
catch { message.error('加载工单失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadOrders(); }, []);
useEffect(() => { loadOrders(); }, [typeFilter]);
const handleCreate = async (values: any) => {
try {
await createMaintenanceOrder(values);
const payload = {
...values,
due_date: values.due_date ? values.due_date.format('YYYY-MM-DD') : undefined,
};
await createMaintenanceOrder(payload);
message.success('工单创建成功');
setShowModal(false);
form.resetFields();
@@ -255,7 +270,12 @@ function OrdersTab() {
const columns = [
{ title: '工单号', dataIndex: 'code', width: 160 },
{ title: '类型', dataIndex: 'order_type', width: 100, render: (v: string) => {
const t = orderTypeMap[v] || { label: v || '消缺', color: 'default' };
return <Tag color={t.color}>{t.label}</Tag>;
}},
{ title: '标题', dataIndex: 'title' },
{ title: '电站', dataIndex: 'station_name', width: 120, render: (v: string) => v || '-' },
{ title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => {
const p = priorityMap[v] || { color: 'default', text: v };
return <Tag color={p.color}>{p.text}</Tag>;
@@ -265,6 +285,7 @@ function OrdersTab() {
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: '要求完成', dataIndex: 'due_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ 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>}
@@ -274,16 +295,28 @@ function OrdersTab() {
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Card size="small" extra={
<Space>
<Select allowClear placeholder="工单类型" style={{ width: 130 }} value={typeFilter} onChange={(v) => { setTypeFilter(v); }}
options={Object.entries(orderTypeMap).map(([k, v]) => ({ label: v.label, value: k }))} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>
</Space>
}>
<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="取消">
<Modal title="新建工单" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消">
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="order_type" label="工单类型" initialValue="repair" rules={[{ required: true }]}>
<Select options={Object.entries(orderTypeMap).map(([k, v]) => ({ label: v.label, value: k }))} />
</Form.Item>
<Form.Item name="title" label="工单标题" rules={[{ required: true }]}>
<Input placeholder="例: 2#热泵压缩机异响" />
</Form.Item>
<Form.Item name="description" label="故障描述">
<Form.Item name="station_name" label="电站名称">
<Input placeholder="中关村医疗器械园26号院" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="device_id" label="关联设备ID">
@@ -292,6 +325,9 @@ function OrdersTab() {
<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="due_date" label="要求完成日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="cost_estimate" label="预估费用">
<InputNumber style={{ width: '100%' }} prefix="¥" min={0} />
</Form.Item>
@@ -388,10 +424,10 @@ export default function Maintenance() {
return (
<div>
<Tabs items={[
{ key: 'dashboard', label: '概览', children: <DashboardTab /> },
{ key: 'dashboard', label: '运维概览', children: <DashboardTab /> },
{ key: 'orders', label: '工单管理', children: <OrdersTab /> },
{ 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,269 @@
import { useEffect, useState } from 'react';
import {
Card, Form, Input, InputNumber, Switch, Select, Button, Slider, Row, Col,
Space, message, Alert, Tag, Descriptions, Spin, Divider,
} from 'antd';
import {
ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import { getSettings, updateSettings, testAiConnection } from '../../services/api';
import { getUser } from '../../utils/auth';
const DEFAULT_SYSTEM_PROMPT = '你是一个专业的光伏电站智能运维助手。你的任务是分析光伏电站的设备运行数据、告警信息和历史趋势,提供专业的诊断分析和运维建议。请用中文回答,结构清晰,重点突出。';
const DEFAULT_DIAGNOSTIC_PROMPT = '请分析以下光伏设备的运行数据,给出诊断报告:\n\n设备信息{device_info}\n运行数据{metrics}\n告警记录{alarms}\n\n请按以下结构输出\n## 运行概况\n## 问题诊断\n## 建议措施\n## 风险预警';
const DEFAULT_INSIGHT_PROMPT = '请根据以下电站运行数据,生成运营洞察报告:\n\n电站概况{station_info}\n关键指标{kpis}\n近期告警{recent_alarms}\n\n请给出3-5条关键洞察和建议。';
const PROMPT_PRESETS: Record<string, { label: string; system: string }> = {
default: { label: '恢复默认', system: DEFAULT_SYSTEM_PROMPT },
expert: { label: '光伏专家模式', system: '你是资深光伏电站运维专家拥有10年以上分布式光伏电站运维经验。请基于你的专业知识对设备数据进行深度分析特别关注组件衰减、逆变器效率、PR值变化和组串失配等关键问题。回答要专业且具有可操作性。' },
assistant: { label: '运维助手模式', system: '你是一个友好的光伏电站运维助手。请用简洁易懂的语言,帮助运维人员理解设备运行状况,并给出清晰的操作建议。避免过于专业的术语,重点关注需要立即处理的问题。' },
};
export default function AIModelSettings() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<any>(null);
const user = getUser();
const isAdmin = user?.role === 'admin';
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
setLoading(true);
try {
const data = await getSettings() as any;
form.setFieldsValue({
ai_enabled: data.ai_enabled,
ai_provider: data.ai_provider || 'stepfun',
ai_api_base_url: data.ai_api_base_url || '',
ai_api_key: '', // Don't show masked key in input
ai_model_name: data.ai_model_name || 'step-2-16k',
ai_temperature: data.ai_temperature ?? 0.7,
ai_max_tokens: data.ai_max_tokens ?? 2000,
ai_context_length: data.ai_context_length ?? 8000,
ai_fallback_enabled: data.ai_fallback_enabled,
ai_fallback_provider: data.ai_fallback_provider || 'zhipu',
ai_fallback_api_base_url: data.ai_fallback_api_base_url || '',
ai_fallback_api_key: '', // Don't show masked key
ai_fallback_model_name: data.ai_fallback_model_name || 'codegeex-4',
ai_system_prompt: data.ai_system_prompt || DEFAULT_SYSTEM_PROMPT,
ai_diagnostic_prompt: data.ai_diagnostic_prompt || DEFAULT_DIAGNOSTIC_PROMPT,
ai_insight_prompt: data.ai_insight_prompt || DEFAULT_INSIGHT_PROMPT,
});
} catch { message.error('加载AI设置失败'); }
finally { setLoading(false); }
};
const handleSave = async () => {
setSaving(true);
try {
const values = form.getFieldsValue();
// Only include api_key fields if user actually typed something (not empty)
const updates: any = { ...values };
if (!updates.ai_api_key) delete updates.ai_api_key;
if (!updates.ai_fallback_api_key) delete updates.ai_fallback_api_key;
await updateSettings(updates);
message.success('AI设置已保存');
} catch { message.error('保存失败'); }
finally { setSaving(false); }
};
const handleTest = async () => {
// Save first, then test
setTesting(true);
setTestResult(null);
try {
const values = form.getFieldsValue();
const updates: any = { ...values };
if (!updates.ai_api_key) delete updates.ai_api_key;
if (!updates.ai_fallback_api_key) delete updates.ai_fallback_api_key;
await updateSettings(updates);
const result = await testAiConnection();
setTestResult(result);
} catch (e: any) { message.error('测试失败: ' + (e?.detail || '未知错误')); }
finally { setTesting(false); }
};
const applyPreset = (key: string) => {
const preset = PROMPT_PRESETS[key];
if (preset) {
form.setFieldsValue({ ai_system_prompt: preset.system });
if (key === 'default') {
form.setFieldsValue({
ai_diagnostic_prompt: DEFAULT_DIAGNOSTIC_PROMPT,
ai_insight_prompt: DEFAULT_INSIGHT_PROMPT,
});
}
message.info(`已应用「${preset.label}」提示词模板`);
}
};
if (loading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
return (
<Form form={form} layout="vertical" disabled={!isAdmin}>
<Row gutter={[16, 16]}>
{/* Primary Model Connection */}
<Col xs={24} lg={12}>
<Card title={<><ApiOutlined /> </>} size="small"
extra={
<Button type="primary" icon={<ThunderboltOutlined />}
loading={testing} onClick={handleTest} disabled={!isAdmin}>
</Button>
}>
<Form.Item name="ai_enabled" label="启用AI分析" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="ai_provider" label="模型提供商">
<Select options={[
{ label: 'StepFun (阶跃星辰)', value: 'stepfun' },
{ label: 'ZhipuAI (智谱AI)', value: 'zhipu' },
{ label: 'OpenAI', value: 'openai' },
{ label: '其他 (OpenAI兼容)', value: 'other' },
]} />
</Form.Item>
<Form.Item name="ai_api_base_url" label="API Base URL">
<Input placeholder="https://api.stepfun.com/step_plan/v1" />
</Form.Item>
<Form.Item name="ai_api_key" label="API Key">
<Input.Password placeholder="输入新Key (留空则不修改)" />
</Form.Item>
<Form.Item name="ai_model_name" label="模型名称">
<Select
showSearch allowClear
options={[
{ label: 'step-2-16k (StepFun)', value: 'step-2-16k' },
{ label: 'step-1-8k (StepFun)', value: 'step-1-8k' },
{ label: 'glm-4 (ZhipuAI)', value: 'glm-4' },
{ label: 'codegeex-4 (ZhipuAI)', value: 'codegeex-4' },
{ label: 'gpt-4o (OpenAI)', value: 'gpt-4o' },
]}
dropdownRender={(menu) => <>{menu}<Divider style={{ margin: '4px 0' }} /><div style={{ padding: '4px 8px', fontSize: 12, color: '#999' }}></div></>}
/>
</Form.Item>
</Card>
</Col>
{/* Fallback Model */}
<Col xs={24} lg={12}>
<Card title="备用模型 (Fallback)" size="small">
<Form.Item name="ai_fallback_enabled" label="启用备用模型" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="ai_fallback_provider" label="备用提供商">
<Select options={[
{ label: 'ZhipuAI (智谱AI)', value: 'zhipu' },
{ label: 'StepFun (阶跃星辰)', value: 'stepfun' },
{ label: 'OpenAI', value: 'openai' },
{ label: '其他', value: 'other' },
]} />
</Form.Item>
<Form.Item name="ai_fallback_api_base_url" label="备用 API Base URL">
<Input placeholder="https://open.bigmodel.cn/api/coding/paas/v4" />
</Form.Item>
<Form.Item name="ai_fallback_api_key" label="备用 API Key">
<Input.Password placeholder="输入新Key (留空则不修改)" />
</Form.Item>
<Form.Item name="ai_fallback_model_name" label="备用模型名称">
<Select showSearch allowClear options={[
{ label: 'codegeex-4 (ZhipuAI)', value: 'codegeex-4' },
{ label: 'glm-4 (ZhipuAI)', value: 'glm-4' },
{ label: 'step-1-8k (StepFun)', value: 'step-1-8k' },
]} />
</Form.Item>
{/* Test Results */}
{testResult && (
<div style={{ marginTop: 16 }}>
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="主模型">
{testResult.primary?.status === 'success' ?
<Tag icon={<CheckCircleOutlined />} color="success"> ({testResult.primary.model})</Tag> :
<Tag icon={<CloseCircleOutlined />} color="error">: {testResult.primary?.error?.slice(0, 80)}</Tag>
}
</Descriptions.Item>
<Descriptions.Item label="备用模型">
{testResult.fallback?.status === 'success' ?
<Tag icon={<CheckCircleOutlined />} color="success"> ({testResult.fallback.model})</Tag> :
testResult.fallback?.status === 'unknown' ?
<Tag color="default"></Tag> :
<Tag icon={<CloseCircleOutlined />} color="error">: {testResult.fallback?.error?.slice(0, 80)}</Tag>
}
</Descriptions.Item>
</Descriptions>
</div>
)}
</Card>
</Col>
{/* Model Parameters */}
<Col xs={24} lg={12}>
<Card title="模型参数" size="small">
<Form.Item name="ai_temperature" label="Temperature (创造性)">
<Slider min={0} max={1} step={0.1} marks={{ 0: '精确', 0.5: '平衡', 1: '创造' }} />
</Form.Item>
<Form.Item name="ai_max_tokens" label="最大输出 Tokens">
<InputNumber min={100} max={8000} step={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="ai_context_length" label="上下文长度 (Tokens)">
<InputNumber min={1000} max={128000} step={1000} style={{ width: '100%' }} />
</Form.Item>
</Card>
</Col>
{/* Prompt Configuration */}
<Col xs={24} lg={12}>
<Card title="提示词配置" size="small"
extra={
<Space>
{Object.entries(PROMPT_PRESETS).map(([key, preset]) => (
<Button key={key} size="small" onClick={() => applyPreset(key)}>
{preset.label}
</Button>
))}
</Space>
}>
<Form.Item name="ai_system_prompt" label="系统提示词 (System Prompt)">
<Input.TextArea rows={4} placeholder="定义AI助手的角色和行为..." />
</Form.Item>
</Card>
</Col>
<Col xs={24}>
<Card title="分析提示词模板" size="small">
<Alert message="支持变量: {device_info} {metrics} {alarms} {station_info} {kpis} {recent_alarms}" type="info" showIcon style={{ marginBottom: 16 }} />
<Row gutter={16}>
<Col xs={24} lg={12}>
<Form.Item name="ai_diagnostic_prompt" label="设备诊断提示词">
<Input.TextArea rows={6} placeholder="设备级诊断分析提示词模板..." />
</Form.Item>
</Col>
<Col xs={24} lg={12}>
<Form.Item name="ai_insight_prompt" label="运营洞察提示词">
<Input.TextArea rows={6} placeholder="电站级运营洞察提示词模板..." />
</Form.Item>
</Col>
</Row>
</Card>
</Col>
</Row>
<div style={{ marginTop: 16, textAlign: 'right' }}>
<Space>
<Button onClick={loadSettings} icon={<ReloadOutlined />}></Button>
<Button type="primary" onClick={handleSave} loading={saving} disabled={!isAdmin}>
</Button>
</Space>
</div>
</Form>
);
}

View File

@@ -5,6 +5,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { getUsers, createUser, updateUser, getRoles } from '../../services/api';
import AuditLog from './AuditLog';
import SystemSettings from './Settings';
import AIModelSettings from './AIModelSettings';
import { RobotOutlined } from '@ant-design/icons';
export default function SystemManagement() {
const [users, setUsers] = useState<any>({ total: 0, items: [] });
@@ -22,6 +24,7 @@ export default function SystemManagement() {
users: 'users',
roles: 'roles',
settings: 'settings',
'ai-models': 'ai-models',
audit: 'audit',
};
const activeTab = tabKeyMap[pathSegment] || 'users';
@@ -107,6 +110,7 @@ export default function SystemManagement() {
</Card>
)},
{ key: 'settings', label: '系统设置', children: <SystemSettings /> },
{ key: 'ai-models', label: 'AI模型配置', children: <AIModelSettings /> },
{ key: 'audit', label: '审计日志', children: <AuditLog /> },
]}
/>

View File

@@ -0,0 +1,236 @@
import { useEffect, useState } from 'react';
import {
Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber,
Space, message, Row, Col, Statistic,
} from 'antd';
import { PlusOutlined, ShopOutlined, WarningOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import {
getSpareParts, createSparePart, updateSparePart,
getWarehouseTransactions, createWarehouseTransaction, getWarehouseStats,
} from '../../services/api';
// ── Tab 1: Spare Parts ────────────────────────────────────────────
function SparePartsTab() {
const [parts, setParts] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [keyword, setKeyword] = useState('');
const [showModal, setShowModal] = useState(false);
const [editingPart, setEditingPart] = useState<any>(null);
const [form] = Form.useForm();
const [stats, setStats] = useState<any>(null);
const loadParts = async () => {
setLoading(true);
try { setParts(await getSpareParts({ keyword: keyword || undefined })); }
catch { message.error('加载备件列表失败'); }
finally { setLoading(false); }
};
useEffect(() => {
(async () => {
try { setStats(await getWarehouseStats()); } catch {}
})();
}, []);
useEffect(() => { loadParts(); }, [keyword]);
const handleSubmit = async (values: any) => {
try {
if (editingPart) {
await updateSparePart(editingPart.id, values);
message.success('备件更新成功');
} else {
await createSparePart(values);
message.success('备件创建成功');
}
setShowModal(false);
setEditingPart(null);
form.resetFields();
loadParts();
} catch { message.error(editingPart ? '更新失败' : '创建失败'); }
};
const openEdit = (record: any) => {
setEditingPart(record);
form.setFieldsValue(record);
setShowModal(true);
};
const columns = [
{ title: '备件编码', dataIndex: 'part_code', width: 130 },
{ title: '名称', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category', width: 100 },
{ title: '规格', dataIndex: 'specification', width: 120 },
{ title: '当前库存', dataIndex: 'current_stock', width: 100, render: (v: number, r: any) => (
<span style={{ color: v <= (r.min_stock || 0) ? '#f5222d' : undefined, fontWeight: v <= (r.min_stock || 0) ? 'bold' : undefined }}>
{v}
</span>
)},
{ title: '单价', dataIndex: 'unit_price', width: 90, render: (v: number) => v != null ? `¥${v.toFixed(2)}` : '-' },
{ title: '供应商', dataIndex: 'supplier', width: 120 },
{ title: '操作', key: 'action', width: 80, render: (_: any, r: any) => (
<Button size="small" onClick={() => openEdit(r)}></Button>
)},
];
return (
<div>
{stats && (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}><Card><Statistic title="备件种类" value={stats.total_parts || 0} prefix={<ShopOutlined />} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="库存预警" value={stats.low_stock_count || 0} prefix={<WarningOutlined />} valueStyle={{ color: (stats.low_stock_count || 0) > 0 ? '#f5222d' : undefined }} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="本月入库" value={stats.monthly_in || 0} valueStyle={{ color: '#52c41a' }} /></Card></Col>
<Col xs={12} sm={6}><Card><Statistic title="本月出库" value={stats.monthly_out || 0} valueStyle={{ color: '#fa8c16' }} /></Card></Col>
</Row>
)}
<Card size="small" extra={
<Space>
<Input.Search placeholder="搜索备件" allowClear onSearch={setKeyword} style={{ width: 200 }} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingPart(null); form.resetFields(); setShowModal(true); }}></Button>
</Space>
}>
<Table columns={columns} dataSource={Array.isArray(parts) ? parts : (parts.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: parts.total, pageSize: 20 }} />
</Card>
<Modal title={editingPart ? '编辑备件' : '新增备件'} open={showModal} onCancel={() => { setShowModal(false); setEditingPart(null); }} onOk={() => form.submit()} okText={editingPart ? '保存' : '创建'} cancelText="取消" width={600}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="part_code" label="备件编码" rules={[{ required: true }]}>
<Input placeholder="例: SP-001" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="category" label="分类">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="specification" label="规格">
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="current_stock" label="当前库存">
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="min_stock" label="最低库存">
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="unit_price" label="单价">
<InputNumber style={{ width: '100%' }} min={0} prefix="¥" />
</Form.Item>
</Col>
</Row>
<Form.Item name="supplier" label="供应商">
<Input />
</Form.Item>
</Form>
</Modal>
</div>
);
}
// ── Tab 2: Transactions ───────────────────────────────────────────
function TransactionsTab() {
const [transactions, setTransactions] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [typeFilter, setTypeFilter] = useState<string | undefined>();
const [showModal, setShowModal] = useState(false);
const [form] = Form.useForm();
const loadTransactions = async () => {
setLoading(true);
try { setTransactions(await getWarehouseTransactions({ type: typeFilter })); }
catch { message.error('加载出入库记录失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadTransactions(); }, [typeFilter]);
const handleCreate = async (values: any) => {
try {
await createWarehouseTransaction(values);
message.success('记录创建成功');
setShowModal(false);
form.resetFields();
loadTransactions();
} catch { message.error('创建失败'); }
};
const columns = [
{ title: '日期', dataIndex: 'transaction_date', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '备件名称', dataIndex: 'part_name' },
{ title: '类型', dataIndex: 'type', width: 80, render: (v: string) => (
<Tag color={v === 'in' ? 'green' : 'red'}>{v === 'in' ? '入库' : '出库'}</Tag>
)},
{ title: '数量', dataIndex: 'quantity', width: 80 },
{ title: '单价', dataIndex: 'unit_price', width: 90, render: (v: number) => v != null ? `¥${v.toFixed(2)}` : '-' },
{ title: '总价', dataIndex: 'total_price', width: 100, render: (v: number) => v != null ? `¥${v.toFixed(2)}` : '-' },
{ title: '原因', dataIndex: 'reason' },
{ title: '操作人', dataIndex: 'operator', width: 100 },
];
return (
<Card size="small" extra={
<Space>
<Select allowClear placeholder="类型筛选" style={{ width: 120 }} value={typeFilter} onChange={setTypeFilter}
options={[{ label: '入库', value: 'in' }, { label: '出库', value: 'out' }]} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>
</Space>
}>
<Table columns={columns} dataSource={Array.isArray(transactions) ? transactions : (transactions.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: transactions.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="type" label="类型" initialValue="in" rules={[{ required: true }]}>
<Select options={[{ label: '入库', value: 'in' }, { label: '出库', value: 'out' }]} />
</Form.Item>
<Form.Item name="spare_part_id" label="备件ID" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="quantity" label="数量" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} min={1} />
</Form.Item>
<Form.Item name="unit_price" label="单价">
<InputNumber style={{ width: '100%' }} min={0} prefix="¥" />
</Form.Item>
<Form.Item name="reason" label="原因">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
// ── Main Component ────────────────────────────────────────────────
export default function WarehouseManagement() {
return (
<div>
<Tabs items={[
{ key: 'parts', label: '备品备件', children: <SparePartsTab /> },
{ key: 'transactions', label: '出入库记录', children: <TransactionsTab /> },
]} />
</div>
);
}

View File

@@ -0,0 +1,122 @@
import { useEffect, useState } from 'react';
import {
Card, Table, Tag, Button, Modal, Form, Input, Select,
Space, message, Badge, DatePicker,
} from 'antd';
import { PlusOutlined, CaretRightOutlined, DeleteOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { getWorkPlans, createWorkPlan, deleteWorkPlan, triggerWorkPlan } from '../../services/api';
const planTypeMap: Record<string, { color: string; text: string }> = {
inspection: { color: 'blue', text: '巡检' },
meter_reading: { color: 'green', text: '抄表' },
other: { color: 'default', text: '其他' },
};
const cycleMap: Record<string, string> = {
daily: '每日',
weekly: '每周',
monthly: '每月',
quarterly: '每季',
yearly: '每年',
};
export default function WorkPlanManagement() {
const [plans, setPlans] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [form] = Form.useForm();
const loadPlans = async () => {
setLoading(true);
try { setPlans(await getWorkPlans()); }
catch { message.error('加载工作计划失败'); }
finally { setLoading(false); }
};
useEffect(() => { loadPlans(); }, []);
const handleCreate = async (values: any) => {
try {
if (values.effective_start) values.effective_start = values.effective_start.format('YYYY-MM-DD');
if (values.effective_end) values.effective_end = values.effective_end.format('YYYY-MM-DD');
await createWorkPlan(values);
message.success('工作计划创建成功');
setShowModal(false);
form.resetFields();
loadPlans();
} catch { message.error('创建失败'); }
};
const handleTrigger = async (id: number) => {
try {
await triggerWorkPlan(id);
message.success('已手动触发');
} catch { message.error('触发失败'); }
};
const handleDelete = async (id: number) => {
try {
await deleteWorkPlan(id);
message.success('已删除');
loadPlans();
} catch { message.error('删除失败'); }
};
const columns = [
{ title: '计划名称', dataIndex: 'name' },
{ title: '计划类型', dataIndex: 'plan_type', width: 90, render: (v: string) => {
const t = planTypeMap[v] || { color: 'default', text: v };
return <Tag color={t.color}>{t.text}</Tag>;
}},
{ title: '场站', dataIndex: 'station_name', width: 120 },
{ title: '执行周期', dataIndex: 'cycle_period', width: 90, render: (v: string) => cycleMap[v] || v || '-' },
{ title: '状态', dataIndex: 'is_active', width: 80, render: (v: boolean) => (
<Badge status={v ? 'success' : 'default'} text={v ? '启用' : '停用'} />
)},
{ title: '生效开始', dataIndex: 'effective_start', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '生效结束', dataIndex: 'effective_end', width: 120, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-' },
{ title: '操作', key: 'action', width: 160, render: (_: any, r: any) => (
<Space>
<Button size="small" type="primary" icon={<CaretRightOutlined />} onClick={() => handleTrigger(r.id)}></Button>
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r.id)}></Button>
</Space>
)},
];
return (
<Card size="small" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>}>
<Table columns={columns} dataSource={Array.isArray(plans) ? plans : (plans.items || [])} rowKey="id" loading={loading} size="small"
pagination={{ total: plans.total, pageSize: 20 }} />
<Modal title="新建工作计划" open={showModal} onCancel={() => setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消" width={600}>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="name" label="计划名称" rules={[{ required: true }]}>
<Input placeholder="例: 每日热泵巡检" />
</Form.Item>
<Form.Item name="plan_type" label="计划类型" initialValue="inspection">
<Select options={Object.entries(planTypeMap).map(([k, v]) => ({ label: v.text, value: k }))} />
</Form.Item>
<Form.Item name="station_name" label="场站名称">
<Input />
</Form.Item>
<Form.Item name="cycle_period" label="执行周期" initialValue="daily">
<Select options={Object.entries(cycleMap).map(([k, v]) => ({ label: v, value: k }))} />
</Form.Item>
<Form.Item name="execution_days" label="执行日">
<Input placeholder="例: 1,15 (每月1号和15号)" />
</Form.Item>
<Form.Item name="effective_start" label="生效开始">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="effective_end" label="生效结束">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}

View File

@@ -359,4 +359,47 @@ export const getWeatherImpact = (params?: Record<string, any>) => api.get('/weat
export const getWeatherConfig = () => api.get('/weather/config');
export const updateWeatherConfig = (data: any) => api.put('/weather/config', data);
// Assets (资产管理)
export const getAssets = (params?: Record<string, any>) => api.get('/assets', { params });
export const getAsset = (id: number) => api.get(`/assets/${id}`);
export const createAsset = (data: any) => api.post('/assets', data);
export const updateAsset = (id: number, data: any) => api.put(`/assets/${id}`, data);
export const deleteAsset = (id: number) => api.delete(`/assets/${id}`);
export const getAssetCategories = () => api.get('/assets/categories');
export const createAssetCategory = (data: any) => api.post('/assets/categories', data);
export const getAssetStats = () => api.get('/assets/stats');
export const getAssetChanges = (params?: Record<string, any>) => api.get('/assets/changes', { params });
export const createAssetChange = (data: any) => api.post('/assets/changes', data);
// Warehouse (仓库管理)
export const getSpareParts = (params?: Record<string, any>) => api.get('/warehouse/parts', { params });
export const getSparePart = (id: number) => api.get(`/warehouse/parts/${id}`);
export const createSparePart = (data: any) => api.post('/warehouse/parts', data);
export const updateSparePart = (id: number, data: any) => api.put(`/warehouse/parts/${id}`, data);
export const getWarehouseTransactions = (params?: Record<string, any>) => api.get('/warehouse/transactions', { params });
export const createWarehouseTransaction = (data: any) => api.post('/warehouse/transactions', data);
export const getWarehouseStats = () => api.get('/warehouse/stats');
// Work Plans (工作计划)
export const getWorkPlans = (params?: Record<string, any>) => api.get('/work-plans', { params });
export const getWorkPlan = (id: number) => api.get(`/work-plans/${id}`);
export const createWorkPlan = (data: any) => api.post('/work-plans', data);
export const updateWorkPlan = (id: number, data: any) => api.put(`/work-plans/${id}`, data);
export const deleteWorkPlan = (id: number) => api.delete(`/work-plans/${id}`);
export const triggerWorkPlan = (id: number) => api.post(`/work-plans/${id}/trigger`);
// Billing (电费结算)
export const getBillingRecords = (params?: Record<string, any>) => api.get('/billing', { params });
export const getBillingRecord = (id: number) => api.get(`/billing/${id}`);
export const createBillingRecord = (data: any) => api.post('/billing', data);
export const updateBillingRecord = (id: number, data: any) => api.put(`/billing/${id}`, data);
export const getBillingStats = (params?: Record<string, any>) => api.get('/billing/stats', { params });
// AI Settings & Analysis
export const testAiConnection = () => api.post('/settings/test-ai');
export const aiAnalyze = (params: { scope: string; device_id?: number }) =>
api.post('/ai-ops/analyze', null, { params, timeout: 60000 });
export const getAiAnalysisHistory = (params?: Record<string, any>) =>
api.get('/ai-ops/analysis-history', { params });
export default api;