import React, { useState, useEffect } from 'react'; import { Card, Row, Col, Statistic, Table, Tag, DatePicker, Space, Select, Progress, message, Button, } from 'antd'; import { InboxOutlined, ExportOutlined, WarningOutlined, DollarOutlined, ShoppingCartOutlined, ExclamationCircleOutlined, PlusOutlined, BarChartOutlined, DownloadOutlined, } from '@ant-design/icons'; import axios from 'axios'; import dayjs from 'dayjs'; const { RangePicker } = DatePicker; const { Option } = Select; const designTokens = { colors: { primary: { main: '#667eea', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', light: '#8b9ff0', }, success: { main: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', }, warning: { main: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', }, error: { main: '#ef4444', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', }, text: { primary: '#1e293b', secondary: '#64748b', tertiary: '#94a3b8', inverse: '#ffffff', }, background: { primary: '#ffffff', secondary: '#f8fafc', tertiary: '#f1f5f9', }, border: { light: '#e2e8f0', medium: '#cbd5e1', dark: '#94a3b8', }, }, shadows: { small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', }, borderRadius: { small: '6px', medium: '10px', large: '16px', }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', }, }; const pageContainerStyle = { minHeight: '100vh', background: designTokens.colors.background.secondary, padding: designTokens.spacing.lg, }; const headerStyle = { marginBottom: designTokens.spacing.lg, padding: `${designTokens.spacing.lg}px ${designTokens.spacing.xl}px`, background: designTokens.colors.background.primary, borderRadius: designTokens.borderRadius.large, boxShadow: designTokens.shadows.small, border: `1px solid ${designTokens.colors.border.light}`, }; const titleRowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: designTokens.spacing.md, flexWrap: 'wrap', gap: designTokens.spacing.md, }; const titleStyle = { display: 'flex', alignItems: 'center', gap: designTokens.spacing.sm, fontSize: '20px', fontWeight: '600', color: designTokens.colors.text.primary, }; const statsRowStyle = { display: 'flex', gap: designTokens.spacing.md, flexWrap: 'wrap', }; const statCardStyle = { background: designTokens.colors.background.primary, borderRadius: designTokens.borderRadius.medium, padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`, boxShadow: designTokens.shadows.small, border: `1px solid ${designTokens.colors.border.light}`, minWidth: '180px', flex: 1, }; const statCardTextStyle = { fontSize: '13px', color: designTokens.colors.text.secondary, marginBottom: designTokens.spacing.xs, }; const statCardValueStyle = { fontSize: '28px', fontWeight: '600', color: designTokens.colors.text.primary, }; const statCardIconStyle = color => ({ fontSize: '28px', color: color, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '48px', height: '48px', borderRadius: designTokens.borderRadius.medium, background: `${color}12`, }); const panelStyle = { background: designTokens.colors.background.primary, borderRadius: designTokens.borderRadius.large, boxShadow: designTokens.shadows.small, border: `1px solid ${designTokens.colors.border.light}`, overflow: 'hidden', }; const panelHeaderStyle = { padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`, borderBottom: `1px solid ${designTokens.colors.border.light}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', }; const panelTitleStyle = { fontSize: '15px', fontWeight: '600', color: designTokens.colors.text.primary, display: 'flex', alignItems: 'center', gap: designTokens.spacing.sm, }; const panelBodyStyle = { padding: designTokens.spacing.lg, }; const actionButtonStyle = { height: '36px', padding: `0 ${designTokens.spacing.md}px`, borderRadius: designTokens.borderRadius.small, fontSize: '13px', display: 'flex', alignItems: 'center', gap: designTokens.spacing.xs, }; const primaryActionStyle = { ...actionButtonStyle, background: designTokens.colors.primary.gradient, border: 'none', color: '#ffffff', boxShadow: designTokens.shadows.small, }; function ConsumableStatistics() { const [summary, setSummary] = useState({ total: 0, lowStock: 0, totalValue: 0, byCategory: [] }); const [lowStockItems, setLowStockItems] = useState([]); const [stats, setStats] = useState({ inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0, recentRecords: [], }); const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState([]); const [categoryFilter, setCategoryFilter] = useState(null); const fetchSummary = async () => { try { const response = await axios.get('/api/consumables/statistics/summary'); setSummary(response.data); } catch (error) { message.error('获取统计信息失败'); } }; const fetchLowStock = async () => { try { const response = await axios.get('/api/consumables/low-stock'); setLowStockItems(response.data); } catch (error) { message.error('获取低库存信息失败'); } }; const fetchInOutStats = async () => { try { const params = {}; if (dateRange && dateRange.length === 2) { params.startDate = dateRange[0].toISOString(); params.endDate = dateRange[1].toISOString(); } if (categoryFilter) { params.category = categoryFilter; } const response = await axios.get('/api/consumable-records/statistics', { params }); setStats(response.data); } catch (error) { message.error('获取出入库统计失败'); } }; useEffect(() => { const loadData = async () => { setLoading(true); await Promise.all([fetchSummary(), fetchLowStock(), fetchInOutStats()]); setLoading(false); }; loadData(); }, []); useEffect(() => { fetchInOutStats(); }, [dateRange, categoryFilter]); const lowStockColumns = [ { title: '耗材名称', dataIndex: 'name', key: 'name', width: 150, render: text => ( {text} ), }, { title: '分类', dataIndex: 'category', key: 'category', width: 120, render: category => ( {category} ), }, { title: '当前库存', dataIndex: 'currentStock', key: 'currentStock', width: 100, render: value => ( {value} ), }, { title: '最小库存', dataIndex: 'minStock', key: 'minStock', width: 100, render: value => {value}, }, { title: '单位', dataIndex: 'unit', key: 'unit', width: 80, render: value => {value}, }, { title: '充足率', key: 'rate', width: 140, render: (_, record) => { const rate = Math.min( 100, Math.round((record.currentStock / (record.maxStock || 100)) * 100) ); const status = rate < 30 ? 'exception' : rate < 60 ? 'active' : 'success'; return ( ); }, }, { title: '供应商', dataIndex: 'supplier', key: 'supplier', width: 120, render: value => ( {value || '-'} ), }, ]; const recentColumns = [ { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170, render: date => ( {dayjs(date).format('YYYY-MM-DD HH:mm')} ), }, { title: '耗材名称', dataIndex: ['Consumable', 'name'], key: 'consumableName', width: 140, render: text => {text}, }, { title: '类型', dataIndex: 'type', key: 'type', width: 90, render: type => ( {type === 'in' ? '入库' : '出库'} ), }, { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100, render: (value, record) => ( {record.type === 'in' ? '+' : '-'} {value} ), }, { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100, render: value => ( {value || '-'} ), }, { title: '原因', dataIndex: 'reason', key: 'reason', width: 150, render: value => ( {value || '-'} ), }, ]; const categories = summary.byCategory?.map(item => item.category) || []; const netQuantity = stats.inQuantity - stats.outQuantity; return (
耗材统计
耗材种类
{summary.total}
低库存预警
{summary.lowStock}
库存总价值
¥{parseFloat(summary.totalValue || 0).toLocaleString()}
= 0 ? designTokens.colors.success.main : designTokens.colors.error.main}`, }} >
= 0 ? designTokens.colors.success.main : designTokens.colors.error.main )} >
净入库量
= 0 ? designTokens.colors.success.main : designTokens.colors.error.main, }} > {netQuantity >= 0 ? '+' : ''} {netQuantity}
入库出库统计
近30天
入库次数
{stats.inCount}
+{stats.inQuantity}
出库次数
{stats.outCount}
-{stats.outQuantity}
分类统计
{categories.length}类
{summary.byCategory?.map((item, index) => { const colors = [ designTokens.colors.primary.main, designTokens.colors.success.main, designTokens.colors.warning.main, '#8b5cf6', '#06b6d4', '#ec4899', ]; const color = colors[index % colors.length]; return (
{item.category} {item.count}
); })} {(!summary.byCategory || summary.byCategory.length === 0) && (
暂无分类数据
)}
低库存预警
{lowStockItems.length}项
最近出入库记录
最近
); } export default ConsumableStatistics;