feat: 显示 MKH 数据更新进度 #36

Merged
lunanting merged 1 commits from codex/mkh-refresh-progress-0924 into main 2026-09-24 13:37:15 +08:00

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Alert, Button, Card, Collapse, Dropdown, Input, Modal, Segmented, Select, Space, Spin, Switch,
Alert, Button, Card, Collapse, Dropdown, Input, Modal, Progress, Segmented, Select, Space, Spin, Switch,
Table, Tooltip, Typography, message,
} from 'antd';
import {
@ -37,6 +37,19 @@ const EMPTY_FILTERS = { keyword: '', scope: 'warning' };
// 会合并为每个缓存周期一次业务库读取,不会每 2 分钟重复扫远程大表。
const AUTO_REFRESH_MS = 2 * 60 * 1000;
const getRefreshProgress = (elapsedSeconds) => {
if (elapsedSeconds < 3) {
return { percent: 12 + elapsedSeconds * 8, stage: '正在读取实时库存、箱表和加工单…' };
}
if (elapsedSeconds < 15) {
return { percent: 36 + (elapsedSeconds - 3) * 3, stage: '正在核对待发货订单和近15天销量…' };
}
return {
percent: Math.min(95, 72 + Math.floor((elapsedSeconds - 15) / 2)),
stage: '正在合并最新数据与 BOM 配件结果…',
};
};
// 预警等级 → 标签配色
const WARNING_TONE = {
缺货: 'danger',
@ -266,6 +279,7 @@ const MkhBomData = () => {
// 导出(当前筛选条件下的全部数据)
const [exporting, setExporting] = useState(false);
const [syncing, setSyncing] = useState(false);
const [refreshProgress, setRefreshProgress] = useState({ open: false, elapsedSeconds: 0, title: '' });
// 补货预警推送设置
const [alertOpen, setAlertOpen] = useState(false);
@ -280,10 +294,35 @@ const MkhBomData = () => {
const tableWrapRef = useRef(null);
const listRequestGateRef = useRef(createRequestGate());
const detailGenerationRef = useRef(0);
const refreshProgressGenerationRef = useRef(0);
const lastListRefreshAtRef = useRef(0);
const autoRefreshPendingRef = useRef(false);
const [bodyHeight, setBodyHeight] = useState(420);
const beginRefreshProgress = useCallback((title) => {
refreshProgressGenerationRef.current += 1;
const generation = refreshProgressGenerationRef.current;
setRefreshProgress({ open: true, elapsedSeconds: 0, title, generation });
return generation;
}, []);
const endRefreshProgress = useCallback((generation) => {
if (generation !== refreshProgressGenerationRef.current) return;
setRefreshProgress((current) => ({ ...current, open: false }));
}, []);
useEffect(() => {
if (!refreshProgress.open) return undefined;
const startedAt = Date.now();
const timer = window.setInterval(() => {
setRefreshProgress((current) => ({
...current,
elapsedSeconds: Math.max(0, Math.floor((Date.now() - startedAt) / 1000)),
}));
}, 1000);
return () => window.clearInterval(timer);
}, [refreshProgress.open]);
useEffect(() => {
const el = tableWrapRef.current;
if (!el) return undefined;
@ -320,12 +359,16 @@ const MkhBomData = () => {
const load = useCallback(async ({ background = false } = {}) => {
const requestGeneration = listRequestGateRef.current.begin();
let showProgress = false;
let progressGeneration = null;
if (!background) setLoading(true);
if (!background) setError('');
try {
const now = Date.now();
const forceFresh = !lastListRefreshAtRef.current
|| now - lastListRefreshAtRef.current >= AUTO_REFRESH_MS;
showProgress = forceFresh && !background;
if (showProgress) progressGeneration = beginRefreshProgress('正在更新最新业务数据');
// 首次打开/F5 和定时轮询才重读云库;筛选、分页只复用刚刷新的内存索引。
// 后端仍会合并多用户的并发刷新,不会对业务云库造成重复全量查询。
const result = await getMkhBomList({
@ -347,8 +390,9 @@ const MkhBomData = () => {
setError(requestError?.response?.data?.error || requestError?.message || '加载 MKH-BOM 资料失败');
} finally {
if (!background && listRequestGateRef.current.isCurrent(requestGeneration)) setLoading(false);
if (showProgress) endRefreshProgress(progressGeneration);
}
}, [query]);
}, [beginRefreshProgress, endRefreshProgress, query]);
useEffect(() => { load(); }, [load]);
@ -850,6 +894,7 @@ const MkhBomData = () => {
const handleSync = async () => {
setSyncing(true);
setLoading(true);
const progressGeneration = beginRefreshProgress('正在强制同步全部最新数据');
// 立即废弃同步前发出的列表/明细请求;它们即使稍后返回,也不能覆盖新快照。
listRequestGateRef.current.invalidate();
detailGenerationRef.current += 1;
@ -874,11 +919,31 @@ const MkhBomData = () => {
} finally {
if (listRequestGateRef.current.isCurrent(syncGeneration)) setLoading(false);
setSyncing(false);
endRefreshProgress(progressGeneration);
}
};
const progressView = getRefreshProgress(refreshProgress.elapsedSeconds);
return (
<SycmPage className="mkh-page">
<Modal
open={refreshProgress.open}
title={refreshProgress.title}
footer={null}
closable={false}
keyboard={false}
maskClosable={false}
centered
width={480}
>
<Space direction="vertical" size={14} style={{ width: '100%' }}>
<Progress percent={progressView.percent} status="active" strokeColor="#1677ff" />
<Text>{progressView.stage}</Text>
<Text type="secondary">已用时 {refreshProgress.elapsedSeconds} 秒,完成后页面会自动显示新数据。</Text>
<Text type="secondary">进度为查询阶段估算,实际完成以服务端返回为准。</Text>
</Space>
</Modal>
<header className="mkh-header">
<div className="mkh-header__heading">
<Title level={3}>MKH-BOM 资料</Title>