dev-tmp1
tangjie 2026-08-19 15:34:47 +08:00
parent f9c7339ee1
commit a2456fa2c3
2 changed files with 237 additions and 11 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { import {
Form, Form,
Table, Table,
@ -65,6 +65,9 @@ const CHECK_TYPE_OPTIONS = [
{ label: "年度随机抽查", value: "ANNUAL" }, { label: "年度随机抽查", value: "ANNUAL" },
]; ];
/** 随机抽取动画固定时长(毫秒) */
const LOTTERY_DURATION = 3000;
const getYearOptions = () => { const getYearOptions = () => {
const year = dayjs().year(); const year = dayjs().year();
return [0, 1, 2].map((offset) => { return [0, 1, 2].map((offset) => {
@ -280,6 +283,22 @@ const EvalReportDatabase = (props) => {
const [spotDetailVisible, setSpotDetailVisible] = useState(false); const [spotDetailVisible, setSpotDetailVisible] = useState(false);
const [spotDetailData, setSpotDetailData] = useState(null); const [spotDetailData, setSpotDetailData] = useState(null);
// 随机抽取动画(对齐原型 sampling-lottery
const [lotteryStatus, setLotteryStatus] = useState("idle"); // idle | running | finished
const [lotteryTitle, setLotteryTitle] = useState("正在随机抽取报告");
const [lotteryCount, setLotteryCount] = useState("准备抽取");
const [lotteryName, setLotteryName] = useState("等待开始");
const [lotteryProgress, setLotteryProgress] = useState(0);
const lotteryTimerRef = useRef(null);
const lotteryRollRef = useRef(null);
useEffect(() => {
return () => {
if (lotteryTimerRef.current) clearTimeout(lotteryTimerRef.current);
if (lotteryRollRef.current) clearInterval(lotteryRollRef.current);
};
}, []);
const { const {
regulatorEvalReportPageLoading, regulatorEvalReportPageLoading,
regulatorEvalReportDetailLoading, regulatorEvalReportDetailLoading,
@ -362,19 +381,90 @@ const EvalReportDatabase = (props) => {
planCheckDate: dayjs(), planCheckDate: dayjs(),
}); });
setSpotCheckData([]); setSpotCheckData([]);
setLotteryStatus("idle");
setLotteryProgress(0);
setSpotCheckVisible(true); setSpotCheckVisible(true);
}; };
/** 动画滚动:固定 3 秒,滚动展示候选报告数据,完成后 resolve */
const playLottery = (count, candidates) =>
new Promise((resolve) => {
if (lotteryRollRef.current) clearInterval(lotteryRollRef.current);
if (lotteryTimerRef.current) clearTimeout(lotteryTimerRef.current);
setLotteryStatus("running");
setLotteryTitle("系统正在随机抽取报告");
setLotteryCount(`目标 ${count}`);
setLotteryProgress(0);
const startTime = Date.now();
const rollName = () => {
const item =
candidates[Math.floor(Math.random() * candidates.length)];
setLotteryName(
[item.reportNo || "", item.reportName || ""]
.filter(Boolean)
.join(" · "),
);
};
rollName();
lotteryRollRef.current = setInterval(() => {
rollName();
setLotteryProgress(
Math.min(
100,
Math.round(((Date.now() - startTime) / LOTTERY_DURATION) * 100),
),
);
}, 90);
lotteryTimerRef.current = setTimeout(() => {
clearInterval(lotteryRollRef.current);
lotteryRollRef.current = null;
setLotteryProgress(100);
setLotteryTitle("随机抽取完成");
setLotteryName("抽取结果已锁定,正在生成监督检查单");
setLotteryStatus("finished");
resolve();
}, LOTTERY_DURATION);
});
const handleCloseSpotCheck = () => {
if (lotteryTimerRef.current) clearTimeout(lotteryTimerRef.current);
if (lotteryRollRef.current) clearInterval(lotteryRollRef.current);
setLotteryStatus("idle");
setSpotCheckVisible(false);
};
const handleSpotCheck = async () => { const handleSpotCheck = async () => {
const values = await spotCheckForm.validateFields().catch(() => null); const values = await spotCheckForm.validateFields().catch(() => null);
if (!values) return; if (!values || lotteryStatus === "running") return;
const res = await props.regulatorSpotCheckReport({ const count = values.randomNumber;
// 先查询该机构已报送报告,无数据则不播放动画
const pageRes = await props.regulatorEvalReportPage({
orgId: values.orgId, orgId: values.orgId,
randomNumber: values.randomNumber, current: 1,
size: 200,
}); });
if (res?.success !== false) { const candidates =
setSpotCheckData(res?.data || []); pageRes?.success !== false ? pageRes?.data || [] : [];
message.success(`已随机抽取 ${res?.data?.length || 0} 份报告`); if (!candidates.length) {
message.warning("暂无报告");
return;
}
setSpotCheckData([]);
// 动画与抽取接口并行,两者都完成后再展示抽取结果
const animationDone = playLottery(count, candidates);
const request = props
.regulatorSpotCheckReport({
orgId: values.orgId,
randomNumber: count,
})
.then((res) => (res?.success !== false ? res?.data || [] : []))
.catch(() => []);
document.querySelector(".redb-modal .micro-temp-modal-body")?.scrollTo({ top: 999999, behavior: "smooth" });
const [data] = await Promise.all([request, animationDone]);
setSpotCheckData(data);
setLotteryCount(`已抽取 ${data.length} 份报告`);
if (data.length > 0) {
message.success(`已随机抽取 ${data.length} 份报告`);
setTimeout(() => { setTimeout(() => {
document document
.querySelector(".redb-modal .micro-temp-modal-body") .querySelector(".redb-modal .micro-temp-modal-body")
@ -1445,16 +1535,22 @@ const EvalReportDatabase = (props) => {
<Modal <Modal
title="安评报告随机抽检" title="安评报告随机抽检"
open={spotCheckVisible} open={spotCheckVisible}
onCancel={() => setSpotCheckVisible(false)} onCancel={handleCloseSpotCheck}
footer={ footer={
<Space> <Space>
<Button onClick={() => setSpotCheckVisible(false)}>关闭</Button> <Button onClick={handleCloseSpotCheck}>关闭</Button>
<Button <Button
type="primary" type="primary"
loading={regulatorSpotCheckReportLoading} loading={
regulatorSpotCheckReportLoading ||
regulatorEvalReportPageLoading
}
disabled={lotteryStatus === "running"}
onClick={handleSpotCheck} onClick={handleSpotCheck}
> >
随机抽取并生成检查单 {lotteryStatus === "running"
? "正在随机抽取..."
: "随机抽取并生成检查单"}
</Button> </Button>
</Space> </Space>
} }
@ -1532,6 +1628,25 @@ const EvalReportDatabase = (props) => {
</Row> </Row>
</Form> </Form>
{lotteryStatus !== "idle" && (
<div
className={`redb-lottery${
lotteryStatus === "finished" ? " redb-lottery-finished" : ""
}`}
>
<div className="redb-lottery-head">
<strong>{lotteryTitle}</strong>
<span>{lotteryCount}</span>
</div>
<div className="redb-lottery-window">
<div className="redb-lottery-name">{lotteryName}</div>
</div>
<div className="redb-lottery-progress">
<i style={{ width: `${lotteryProgress}%` }} />
</div>
</div>
)}
{spotCheckData.length > 0 && ( {spotCheckData.length > 0 && (
<> <>
<div className="redb-modal-section"> <div className="redb-modal-section">

View File

@ -235,3 +235,114 @@
gap: 12px; gap: 12px;
margin-bottom: 12px; margin-bottom: 12px;
} }
/* 随机抽取动画 — 原型 .sampling-lottery */
.redb-lottery {
margin-top: 16px;
padding: 16px;
border: 1px solid #bfdbfe;
border-radius: 6px;
background: #f8fbff;
overflow: hidden;
}
.redb-lottery-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.redb-lottery-head strong {
font-size: 14px;
color: #1e3a5f;
}
.redb-lottery-head span {
color: #64748b;
}
.redb-lottery-window {
position: relative;
min-height: 118px;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 48px;
border: 1px solid #dbeafe;
border-radius: 5px;
background: #fff;
text-align: center;
}
.redb-lottery-window::before,
.redb-lottery-window::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 24px;
z-index: 1;
pointer-events: none;
}
.redb-lottery-window::before {
top: 0;
background: linear-gradient(#fff, rgba(255, 255, 255, 0));
}
.redb-lottery-window::after {
bottom: 0;
background: linear-gradient(rgba(255, 255, 255, 0), #fff);
}
.redb-lottery-name {
font-size: 16px;
font-weight: 700;
color: #1d4ed8;
line-height: 1.55;
}
.redb-lottery:not(.redb-lottery-finished) .redb-lottery-name {
animation: redb-sampling-pulse 0.18s ease-in-out infinite alternate;
}
.redb-lottery-finished .redb-lottery-window {
border-color: #86efac;
background: #f0fdf4;
}
.redb-lottery-finished .redb-lottery-name {
color: #15803d;
animation: none;
}
.redb-lottery-progress {
height: 5px;
margin-top: 12px;
border-radius: 9px;
background: #dbeafe;
overflow: hidden;
}
.redb-lottery-progress i {
display: block;
width: 0;
height: 100%;
border-radius: 9px;
background: #2563eb;
transition: width 0.2s ease;
}
@keyframes redb-sampling-pulse {
from {
transform: translateY(-2px);
opacity: 0.72;
}
to {
transform: translateY(2px);
opacity: 1;
}
}