import React, { useState, useEffect } from 'react'; import { Card, Row, Col, Statistic, Table, Tag, Progress, DatePicker, Select, Button, Empty, Tooltip, Badge, Divider, Typography, Space, Avatar, } from 'antd'; import { PieChartOutlined, BarChartOutlined, WarningOutlined, HistoryOutlined, ReloadOutlined, CalendarOutlined, AppstoreOutlined, ArrowUpOutlined, ArrowDownOutlined, BoxPlotOutlined, ExclamationCircleOutlined, ShoppingCartOutlined, DatabaseOutlined, } from '@ant-design/icons'; import { motion, AnimatePresence } from 'framer-motion'; import styled from 'styled-components'; import dayjs from 'dayjs'; import api from '../api'; import { message } from 'antd'; const { Title, Text } = Typography; const { RangePicker } = DatePicker; const { Option } = Select; // 设计令牌 - 与其他页面统一 const designTokens = { colors: { primary: { main: '#6366f1', light: '#818cf8', dark: '#4f46e5', gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', }, secondary: { main: '#ec4899', light: '#f472b6', gradient: 'linear-gradient(135deg, #ec4899 0%, #f97316 100%)', }, success: { main: '#10b981', light: '#34d399', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', }, warning: { main: '#f59e0b', light: '#fbbf24', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', }, error: { main: '#ef4444', light: '#f87171', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', }, info: { main: '#3b82f6', light: '#60a5fa', }, background: { main: '#f8fafc', card: '#ffffff', elevated: '#ffffff', }, text: { primary: '#1e293b', secondary: '#64748b', muted: '#94a3b8', }, border: '#e2e8f0', }, shadows: { small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', glow: '0 0 20px rgba(99, 102, 241, 0.15)', }, borderRadius: { small: '8px', medium: '12px', large: '16px', xl: '20px', full: '9999px', }, }; // 动画配置 const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.08, delayChildren: 0.1, }, }, }; const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 100, damping: 15, }, }, }; const cardHoverVariants = { rest: { scale: 1 }, hover: { scale: 1.02, transition: { type: 'spring', stiffness: 400, damping: 25, }, }, }; // 样式组件 const PageContainer = styled.div` padding: 24px; background: ${designTokens.colors.background.main}; min-height: 100vh; @media (max-width: 768px) { padding: 16px; } `; const PageHeader = styled(motion.div)` margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; `; const TitleSection = styled.div` display: flex; align-items: center; gap: 16px; .icon-wrapper { width: 56px; height: 56px; background: ${designTokens.colors.primary.gradient}; border-radius: ${designTokens.borderRadius.large}; display: flex; align-items: center; justify-content: center; box-shadow: ${designTokens.shadows.glow}; .anticon { font-size: 28px; color: white; } } .title-content { h1 { margin: 0; font-size: 28px; font-weight: 700; background: ${designTokens.colors.primary.gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .subtitle { color: ${designTokens.colors.text.secondary}; font-size: 14px; margin-top: 4px; } } `; const FilterSection = styled(motion.div)` background: ${designTokens.colors.background.card}; padding: 20px 24px; border-radius: ${designTokens.borderRadius.large}; box-shadow: ${designTokens.shadows.small}; margin-bottom: 24px; border: 1px solid ${designTokens.colors.border}; `; const StatsCard = styled(motion.div)` background: ${designTokens.colors.background.card}; border-radius: ${designTokens.borderRadius.large}; padding: 24px; box-shadow: ${designTokens.shadows.small}; border: 1px solid ${designTokens.colors.border}; transition: all 0.3s ease; height: 100%; position: relative; overflow: hidden; &:hover { box-shadow: ${designTokens.shadows.large}; transform: translateY(-2px); } &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: ${props => props.accent || designTokens.colors.primary.gradient}; } .card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; .icon-box { width: 48px; height: 48px; border-radius: ${designTokens.borderRadius.medium}; display: flex; align-items: center; justify-content: center; font-size: 24px; background: ${props => props.iconBg || 'rgba(99, 102, 241, 0.1)'}; color: ${props => props.iconColor || designTokens.colors.primary.main}; } .card-title { font-size: 16px; font-weight: 600; color: ${designTokens.colors.text.primary}; } } .stat-value { font-size: 32px; font-weight: 700; color: ${designTokens.colors.text.primary}; margin-bottom: 8px; background: ${props => props.valueGradient || 'none'}; -webkit-background-clip: ${props => props.valueGradient ? 'text' : 'unset'}; -webkit-text-fill-color: ${props => props.valueGradient ? 'transparent' : 'inherit'}; background-clip: ${props => props.valueGradient ? 'text' : 'unset'}; } .stat-label { font-size: 13px; color: ${designTokens.colors.text.secondary}; } `; const ContentCard = styled(motion.div)` background: ${designTokens.colors.background.card}; border-radius: ${designTokens.borderRadius.large}; box-shadow: ${designTokens.shadows.small}; border: 1px solid ${designTokens.colors.border}; overflow: hidden; height: 100%; transition: all 0.3s ease; &:hover { box-shadow: ${designTokens.shadows.medium}; } .card-header { padding: 20px 24px; border-bottom: 1px solid ${designTokens.colors.border}; display: flex; align-items: center; justify-content: space-between; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); .header-left { display: flex; align-items: center; gap: 12px; .header-icon { width: 40px; height: 40px; border-radius: ${designTokens.borderRadius.medium}; display: flex; align-items: center; justify-content: center; font-size: 20px; background: ${props => props.iconBg || designTokens.colors.primary.gradient}; color: white; } .header-title { font-size: 17px; font-weight: 600; color: ${designTokens.colors.text.primary}; } } .header-extra { color: ${designTokens.colors.text.secondary}; font-size: 13px; } } .card-body { padding: 24px; } `; const StyledTable = styled(Table)` .ant-table { background: transparent; } .ant-table-thead > tr > th { background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); font-weight: 600; color: ${designTokens.colors.text.primary}; border-bottom: 2px solid ${designTokens.colors.border}; padding: 16px; } .ant-table-tbody > tr > td { padding: 16px; border-bottom: 1px solid ${designTokens.colors.border}; } .ant-table-tbody > tr:hover > td { background: rgba(99, 102, 241, 0.04); } .ant-table-row { transition: all 0.2s ease; } `; const ProgressBar = styled.div` .progress-wrapper { display: flex; align-items: center; gap: 12px; .ant-progress { flex: 1; margin: 0; } .progress-text { font-size: 13px; font-weight: 600; color: ${props => props.color || designTokens.colors.text.primary}; min-width: 45px; text-align: right; } } `; const CategoryTag = styled(Tag)` padding: 6px 14px; border-radius: ${designTokens.borderRadius.full}; font-size: 13px; font-weight: 500; border: none; display: inline-flex; align-items: center; gap: 6px; .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; } `; const StatItem = styled.div` text-align: center; padding: 16px; background: ${props => props.bg || 'transparent'}; border-radius: ${designTokens.borderRadius.medium}; transition: all 0.3s ease; &:hover { transform: translateY(-2px); box-shadow: ${designTokens.shadows.small}; } .stat-icon { font-size: 24px; margin-bottom: 8px; color: ${props => props.iconColor || designTokens.colors.primary.main}; } .stat-number { font-size: 24px; font-weight: 700; color: ${designTokens.colors.text.primary}; margin-bottom: 4px; } .stat-label { font-size: 12px; color: ${designTokens.colors.text.secondary}; } `; const EmptyState = styled.div` text-align: center; padding: 48px 24px; color: ${designTokens.colors.text.secondary}; .empty-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.5; } .empty-text { font-size: 16px; margin-bottom: 8px; } .empty-subtext { font-size: 13px; opacity: 0.7; } `; const ConsumableStatistics = () => { const [loading, setLoading] = useState(true); const [stats, setStats] = useState({ inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0, recentRecords: [], }); const [summary, setSummary] = useState({ total: 0, lowStock: 0, totalValue: 0, byCategory: [], }); const [lowStockItems, setLowStockItems] = useState([]); const [dateRange, setDateRange] = useState([dayjs().subtract(30, 'days'), dayjs()]); const [categoryFilter, setCategoryFilter] = useState('all'); // 加载统计数据 const loadStatistics = async () => { try { setLoading(true); const params = { startDate: dateRange[0]?.format('YYYY-MM-DD'), endDate: dateRange[1]?.format('YYYY-MM-DD'), }; // 加载记录统计 const statsResponse = await api.get('/consumable-records/statistics', { params }); setStats(statsResponse || { inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0, recentRecords: [], }); // 加载汇总数据 const summaryResponse = await api.get('/consumables/statistics/summary'); setSummary(summaryResponse || { total: 0, lowStock: 0, totalValue: 0, byCategory: [], }); } catch (error) { const errorMsg = error?.message || error || '未知错误'; message.error('加载统计数据失败: ' + errorMsg); console.error('加载统计数据失败:', error); // 使用默认数据 setStats({ inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0, recentRecords: [], }); setSummary({ total: 0, lowStock: 0, totalValue: 0, byCategory: [], }); } finally { setLoading(false); } }; // 加载低库存预警 const loadLowStockItems = async () => { try { const response = await api.get('/consumables/low-stock'); setLowStockItems(response || []); } catch (error) { console.error('加载低库存预警失败:', error?.message || error); setLowStockItems([]); } }; useEffect(() => { loadStatistics(); loadLowStockItems(); }, []); // 低库存表格列 const lowStockColumns = [ { title: '耗材名称', dataIndex: 'name', key: 'name', render: (text, record) => ( {text} {record.specification} ), }, { title: '当前库存', dataIndex: 'currentStock', key: 'currentStock', align: 'center', render: (currentStock, record) => ( {currentStock} {record.unit} ), }, { title: '最小库存', dataIndex: 'minStock', key: 'minStock', align: 'center', render: (minStock, record) => ( {minStock} {record.unit} ), }, { title: '充足率', key: 'rate', align: 'center', render: (_, record) => { const rate = Math.min(100, Math.round((record.currentStock / (record.maxStock || 100)) * 100)); const status = rate < 30 ? 'exception' : rate < 60 ? 'active' : 'success'; const color = rate < 30 ? designTokens.colors.error.main : rate < 60 ? designTokens.colors.warning.main : designTokens.colors.success.main; return ( {rate}% ); }, }, { title: '供应商', dataIndex: 'supplier', key: 'supplier', render: (value) => ( {value || '-'} ), }, ]; // 最近记录表格列 const recentColumns = [ { title: '操作类型', dataIndex: 'type', key: 'type', render: (type) => ( : } color={type === 'in' ? 'success' : 'processing'} style={{ padding: '4px 12px', borderRadius: designTokens.borderRadius.full, fontWeight: 500, }} > {type === 'in' ? '入库' : '出库'} ), }, { title: '耗材名称', dataIndex: ['Consumable', 'name'], key: 'consumableName', render: (text) => ( {text} ), }, { title: '数量', dataIndex: 'quantity', key: 'quantity', align: 'center', render: (quantity, record) => ( {record.type === 'in' ? '+' : '-'}{quantity} ), }, { title: '操作人', dataIndex: 'operator', key: 'operator', render: (operator) => ( {operator?.charAt(0) || '?'} {operator} ), }, { title: '操作时间', dataIndex: 'createdAt', key: 'createdAt', render: (date) => ( {dayjs(date).format('YYYY-MM-DD HH:mm')} ), }, ]; // 获取类别颜色 const getCategoryColor = (category) => { const colors = { '网络设备': '#6366f1', '线缆': '#10b981', '配件': '#f59e0b', '工具': '#ec4899', '其他': '#6b7280', }; return colors[category] || '#6366f1'; }; // 刷新数据 const handleRefresh = () => { loadStatistics(); loadLowStockItems(); message.success('数据已刷新'); }; return ( {/* 页面标题 */} 耗材统计 实时监控耗材库存状态与流转情况 } onClick={handleRefresh} loading={loading} style={{ height: 40, borderRadius: designTokens.borderRadius.medium, borderColor: designTokens.colors.border, }} > 刷新数据 {/* 筛选区域 */} 时间范围 耗材类别 全部类别 网络设备 线缆 配件 工具 其他 应用筛选 {/* 统计卡片 */} 耗材总数 {summary?.total || 0} 种不同类型耗材 库存预警 {summary?.lowStock || 0} 低于安全库存 本月入库 {stats?.inCount || 0} 笔入库记录 本月出库 {stats?.outCount || 0} 笔出库记录 {/* 入库/出库统计 */} 入库/出库统计 {dateRange[0]?.format('YYYY-MM-DD')} 至 {dateRange[1]?.format('YYYY-MM-DD')} {stats?.inCount || 0} 入库次数 {stats?.outCount || 0} 出库次数 {stats?.inQuantity || 0} 入库数量 {stats?.outQuantity || 0} 出库数量 {/* 类别统计 */} 类别统计 {summary?.byCategory?.length > 0 ? ( {summary.byCategory.map((item, index) => ( {item.category} {item.count} 种耗材 总库存: {item.totalQuantity || 0} ))} ) : ( 暂无类别统计数据 添加耗材后将自动统计 )} {/* 低库存预警和最近记录 */} 库存预警 库存充足 所有耗材库存均在安全范围内 ), }} /> 最近记录 最近10条 暂无记录 出入库操作后将显示在这里 ), }} /> ); }; export default ConsumableStatistics;