import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Button, Card, Col, Dropdown, Image, Input, Row, Select, Space, Spin, Statistic, Switch, Table, Tabs, Tag, Tooltip, Typography, message, } from 'antd'; import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; import { SycmPage } from '../../components/Common'; import { getModularStockList, getModularStockFilters } from '../../api/modularStockMonitor'; import { exportTableFile } from '../../utils/tableExport'; import './style.css'; const { Title, Text } = Typography; const WARNING_COLOR = { 缺货: 'red', 预警: 'orange', 正常: 'green' }; // 表头带口径说明,避免把「真实值 0」误读成「没数据」 const TipTitle = ({ title, tip }) => ( {title} ); const number = (value, digits = 2) => value === null || value === undefined || value === '' ? '--' : Number(value).toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }); const int = (value) => number(value, 0); const text = (value) => (value === null || value === undefined || value === '' ? '--' : value); const toOptions = (list = []) => (Array.isArray(list) ? list : []) .filter(Boolean) .map((value) => ({ value, label: value })); const EMPTY_FILTERS = { keyword: '', category: '', warningLevel: '', mkhStatus: '' }; const AccessoryStockPanel = () => { const [filters, setFilters] = useState(EMPTY_FILTERS); const [onlyWarning, setOnlyWarning] = useState(false); const [filterOptions, setFilterOptions] = useState({ categories: [], warningLevels: [] }); const [data, setData] = useState({ rows: [], total: 0, page: 1, pageSize: 50, summary: {}, monitorDate: null }); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [sorter, setSorter] = useState({ field: 'realtimeStock', order: 'DESC' }); const tableWrapRef = useRef(null); const [bodyHeight, setBodyHeight] = useState(420); // 表体高度按「表格顶部 → 内容区底部」实测计算,替代纯 CSS calc: // 不同显示器缩放(125%/150%)下 100vh 偏小,写死的 calc 会把表格压到只剩一两行 useEffect(() => { const el = tableWrapRef.current; if (!el) return undefined; // 分页器 + 卡片内边距 + 横向滚动条余量 const RESERVE_BELOW = 104; const measure = () => { const scroller = el.closest('.planning-subsystem-content'); const elRect = el.getBoundingClientRect(); let available; if (scroller) { const scrollerRect = scroller.getBoundingClientRect(); // 加回 scrollTop,抵消滚动对 getBoundingClientRect 的影响 available = scrollerRect.bottom - elRect.top - RESERVE_BELOW + scroller.scrollTop; } else { available = window.innerHeight - elRect.top - RESERVE_BELOW; } const next = Math.max(240, Math.round(available)); setBodyHeight((prev) => (Math.abs(prev - next) > 1 ? next : prev)); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); if (el.parentElement) ro.observe(el.parentElement); window.addEventListener('resize', measure); return () => { ro.disconnect(); window.removeEventListener('resize', measure); }; }, []); useEffect(() => { let cancelled = false; getModularStockFilters() .then((result) => { const payload = result?.data || {}; if (cancelled) return; setFilterOptions({ categories: payload.categories || [], warningLevels: payload.warningLevels || ['缺货', '预警', '正常'], mkhStatuses: payload.mkhStatuses || ['已配置MKH', '未配置MKH'], }); }) .catch(() => {}); return () => { cancelled = true; }; }, []); const query = useMemo(() => { const params = { page, pageSize, sortField: sorter.field, sortOrder: sorter.order, }; Object.entries(filters).forEach(([key, value]) => { if (value) params[key] = value; }); if (onlyWarning) params.onlyWarning = '1'; return params; }, [page, pageSize, sorter, filters, onlyWarning]); const load = useCallback(async () => { setLoading(true); setError(''); try { const result = await getModularStockList(query); setData(result?.data || { rows: [], total: 0, summary: {} }); } catch (requestError) { setError(requestError?.response?.data?.error || requestError?.message || '加载配件库存监控数据失败'); } finally { setLoading(false); } }, [query]); useEffect(() => { load(); }, [load]); const changeFilter = (key) => (value) => { setFilters((old) => ({ ...old, [key]: value || '' })); setPage(1); }; const resetFilters = () => { setFilters(EMPTY_FILTERS); setOnlyWarning(false); setPage(1); }; const columns = useMemo(() => [ { title: '图片', dataIndex: 'imageUrl', width: 56, fixed: 'left', exportable: false, render: (value) => (value ? : --), }, { title: '预警', dataIndex: 'warningLevel', width: 76, sorter: true, render: (value) => (value ? {value} : --), }, { title: , dataIndex: 'mkhStatus', width: 110, sorter: true, render: (value) => { const v = value || '未配置MKH'; return v === '已配置MKH' ? {v} : {v}; }, }, { title: 'SKU编码', dataIndex: 'skuId', width: 176, fixed: 'left', sorter: true, render: (value) => {text(value)}, }, { // 必须显式给 width:antd 在 scroll.x 场景下会把无宽度列压成 0 宽(此前该列不可见的原因) title: '商品名称', dataIndex: 'productName', width: 220, ellipsis: true, sorter: true, render: (value) => {text(value)}, }, { title: , dataIndex: 'inQtyToday', width: 116, align: 'right', render: (value) => (value === null || value === undefined ? -- : int(value)), }, { title: , dataIndex: 'outQtyToday', width: 116, align: 'right', render: (value) => (value === null || value === undefined ? -- : int(value)), }, { title: '实时库存', dataIndex: 'realtimeStock', width: 96, align: 'right', sorter: true, render: int, }, { title: , dataIndex: 'location', width: 88, sorter: true, render: (value) => (value ? {value} : 未维护 ), }, { title: '近7日日均发货量', dataIndex: 'avgShip7d', width: 138, align: 'right', sorter: true, render: (value) => number(value), }, { title: '库存可用天数', dataIndex: 'stockDays', width: 116, align: 'right', sorter: true, render: (value) => number(value, 1), }, { title: '所在仓库', dataIndex: 'warehouse', width: 200, ellipsis: true, sorter: true, render: (value) => {text(value)}, }, ], []); const exportColumns = useMemo( () => columns.filter((column) => column.exportable !== false), [columns] ); const handleExport = async (format) => { if (!data.rows.length) { message.warning('暂无数据可导出'); return; } try { await exportTableFile( format, `配件库存监控_${data.monitorDate || '最新'}`, exportColumns, data.rows, '配件库存监控' ); message.success(`已导出 ${data.rows.length} 条数据(${format.toUpperCase()})`); } catch (exportError) { message.error(exportError?.message || '导出失败'); } }; const summary = data.summary || {}; return ( <> } placeholder="SKU 编码 / 名称" value={filters.keyword} onChange={(e) => changeFilter('keyword')(e.target.value)} style={{ width: 220 }} />