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:
@@ -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 />} />
|
||||
|
||||
@@ -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', '管理体系') },
|
||||
|
||||
@@ -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>
|
||||
|
||||
277
frontend/src/pages/AssetManagement/index.tsx
Normal file
277
frontend/src/pages/AssetManagement/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
189
frontend/src/pages/BillingManagement/index.tsx
Normal file
189
frontend/src/pages/BillingManagement/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
269
frontend/src/pages/System/AIModelSettings.tsx
Normal file
269
frontend/src/pages/System/AIModelSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 /> },
|
||||
]}
|
||||
/>
|
||||
|
||||
236
frontend/src/pages/WarehouseManagement/index.tsx
Normal file
236
frontend/src/pages/WarehouseManagement/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
frontend/src/pages/WorkPlanManagement/index.tsx
Normal file
122
frontend/src/pages/WorkPlanManagement/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user